YouTube moves the goalposts, so the app stops modelling them as its own truth and shows progress against a threshold table that carries the date it was read. - `Models/YppProgress.cs` — pure: three metrics, per-criterion met, and `MeetsPublishedRule`, which encodes the real shape of the rule (subscribers AND either view route). The drawer says "x of 3 criteria met — YouTube reviews eligibility" and spells out the either/or, so "2 of 3" can't be misread. - `Services/YouTubeAnalyticsService.cs` — the two numbers only Analytics publishes: long-form watch hours (trailing 12 months, `creatorContentType==video`) and Shorts-feed views (90 days, `==shorts`). No rows means NO DATA, not zero. - **Thresholds are data with a publication date** (`YppThresholds.AsOfUtc`, shown in the drawer) and a test fails once that reading is more than 180 days old. The 2027-02-01 doubling (4,000h → 8,000h, 10M → 20M Shorts views) resolves from the date, not from an edit. - **No Analytics scope → no invented numbers:** the drawer explains the missing consent instead of drawing two empty bars as zero. Money stays behind 3·20's "Show monetary metrics" toggle. - Fan-funding tier (500 subs) demoted to a single muted line, since the three bars are the ad-revenue path. Tests: `YppProgressTests` — either/or eligibility, subscribers alone insufficient, Shorts as a qualifying route, bars clamp at 100%, the doubling date, the staleness guard, Analytics parsing (no rows = no data), and compact value formatting. FAST gate: 567 passed, 0 failed, 0 warnings.
8.4 KiB
TASK 39 — YPP journey tracker (slice 1: current-scope data)
Status: ✅ Slice 1 Done — shipped 2026-09-22 (Good Dog: ONE integration test)
Idea (creator ruling 2026-09-21, feasibility signed off 2026-09-22)
Track the connected creator's YouTube Partner Program progress inside the app: a "YPP" tab on the Live screen's right edge, just below Stream Settings, sliding out the same way. Everything the creator needs about YPP eligibility and their position ships in-app — no leaving the app for definitions or numbers. The user also holds the YPP ruleset (studio "Earn" tab): 500 subs / 3 valid public uploads-90d for fan funding; 1,000 subs for full ad / Premium revenue; valid public long-form watch hours over trailing 12 months; valid public Shorts views (Shorts-feed origin) over 90 days; 0 active community-guidelines strikes; 2FA on; linked AdSense; long-form bar doubles to 8,000h on 2027-02-01 (see ai.md "Journey tracker").
Slice split (user chose slice-1-then-slice-2): slice 1 uses ONLY the current OAuth scopes
— no re-consent; slice 2 adds the Analytics API (yt-analytics.readonly, one-time Google
re-consent) behind the reserved IChannelStatsProvider seam for watch-hours/Shorts-views +
velocity/ETA. Slice 1 still writes SQLite snapshots on every refresh, so slice 2 has history
from day one.
Honest limits (told to the user before building)
- No public API exposes the Earn-tab counters or the "valid public" filtering. Numbers are API-derived and labeled close-to-but-not-identical to Studio.
- 2FA and AdSense linkage are not readable from ANY YouTube API → in-app self-reported checkboxes with deep links (Google's 2-Step page / the Earn page).
- Channel standing is NOT readable by ordinary apps either — see the scope correction below;
the slice-1 claim that
channels.list auditDetailsgave it "no re-consent" was WRONG and silently 403'd every real refresh until 2026-09-23. subscriberCountis rounded to 3 significant figures by Google.
⚠️ Scope correction (2026-09-23, creator: "YPP refresh — can't reach YouTube. Seriously?")
The refresh 403'd on EVERY real call since slice 1 shipped: channels.list?mine=true &part=statistics,auditDetails,contentDetails returns 403 insufficientPermissions because the
auditDetails part alone requires the youtubepartner-channel-audit scope — which no
ordinary-creator app should hold (it's the MCN content-partner audit privilege, and the token has
a two-week-revocation rule attached). The mock-based test passed; the real API never did.
Reference: https://developers.google.com/youtube/v3/docs/channels/list ("A request that retrieves
the auditDetails part … must provide … youtubepartner-channel-audit").
Fix: dropped auditDetails from the part list (now statistics,contentDetails); standing
flags removed from the snapshot → VM → drawer, replaced by an honest deep-link row ("Channel
standing isn't exposed to YouTube apps — check the Earn page"). The standing columns in the
YppSnapshot SQLite table stay (schema-stable; always false for the new API path). channels.list
failures now log the response body, so the DIAGNOSIS that cost this hunt (bare status code, no
reason) can't hide again. New Good Dog: ChannelStatsServiceTests.CaptureCurrent_RequestsNoAuditDetails_ AndStillParsesTheSnapshot — guards the URL never carries auditDetails and the snapshot still parses.
The same Good Dog then caught a SECOND latent bug the 403 had masked: statistics.* comes back as
JSON strings (subscriberCount "350"), and the raw GetInt64() read throws — now via the
tolerant ReadInt64 (ValueKind-first; JsonElement.TryGetInt64 throws on strings). Recipe in
MyMistakes.
What shipped
Models/YppThresholds.cs— thresholds as versioned date-aware data (never constants, per ai.md): Tier1 (500 / 3,000h / 3M Shorts),Tier2For(when)returns 4,000h/10M before 2027-02-01 and 8,000h/20M from then;UploadsRequiredPer90Days = 3.Models/YppStatSnapshot.cs+Services/LayoutStore.Ypp.cs+ schema v9 → v10 (YppSnapshottable): append-only snapshot history; manual checklist (Ypp.TwoFactorEnabled/Ypp.AdSenseLinked); last-refresh-UTC gate (the once-daily gate a quota-thin analytics slice will need is baked in now).Services/ChannelStatsService.cs—channels.list?mine=true&part=statistics,contentDetails(subs/views/videos + uploads-playlist id;auditDetailsREMOVED 2026-09-23, see the scope correction) thenplaylistItems.list(recent 50) counts uploads in the trailing 90 days. VirtualCaptureCurrentAsync= the test seam. No new scope.Services/YppTrackerViewModel.cs— drawer state +Toggle/CloseDrawerCommand+RefreshCommand(mirrorsLiveBroadcastFormViewModel), plus: signed-out/loading/error states, tier progress bars (subs Tier-1 & Tier-2, uploads), standing info line (deep-link advice, honest about the API gap since 2026-09-23) + manual 2FA/AdSense checkboxes,WhatCountsTexteducation, deep links,EnsureLoadedAsync(once/day gate) + explicit refresh,OnAccountChanged()hook.ViewModels/MainViewModel.Ypp.cs+ ctor —Yppproperty,ChannelStatsFactoryOverridetest seam (mirrorsCameraEnumeratorOverride), and one-open-at-a-time exclusivity wired both directions (opening YPP closes Stream Settings and vice-versa). Sign-in/sign-out/restored-session hooks inMainViewModel.Account.cs.- UI (
Controls/PreviewPane.xaml) —TextPullOutHostis now one drawer bank: the broadcast drawer, then the YPP drawer, then a stacked white-tab column (Stream Settings above YPP). Same animated-width pattern; only one open at a time.MainWindow.xaml.csoutside-click collapse covers both drawers.IsClickInsideDrawer(PreviewPane.cs) unchanged — covers the rail. - ONE integration test —
ytLive.Tests/YppPullOutTests.cs(RealApp + temp DB): (1) connected VM captures a fake snapshot → progress props, snapshot persisted, checklist round-trip, drawer toggles; (2) real MainWindow enforces drawer exclusivity both ways. - Slice-2 placeholder rows (long-form hours / Shorts views) render a clear "arrives with the extended analytics update" state in the drawer.
Deep links (verified stable endpoints; the exact Studio "Earn" tab path is a slice-2 nicety)
- Earn:
https://www.youtube.com/earn - 2FA:
https://myaccount.google.com/signinoptions/two-step-verification - AdSense:
https://support.google.com/youtube/answer/72851
Slice 2 — ✅ SHIPPED 2026-11-05 (the three bars)
Shipped as designed: yt-analytics.readonly behind IWatchMetricsSource, three bars (subscribers,
long-form watch hours, Shorts views) against YppThresholds.Tier2Now, and one line —
"x of 3 criteria met — YouTube reviews eligibility." Notes recorded with it:
- The view routes are either/or, so the line is qualified in the drawer ("subscribers plus watch
hours, or subscribers plus Shorts views");
YppProgress.MeetsPublishedRuleencodes that shape. - Thresholds carry a publication date (
YppThresholds.AsOfUtc, shown in the drawer) and a test fails once that reading is older than 180 days — YouTube moves the bar and a stale bar must not look current. The 2027-02-01 doubling is data, not a code change. - No analytics scope → no invented numbers: the drawer says what is missing instead of drawing empty bars as zero.
- Money stays behind the drawer's "Show monetary metrics" toggle (TASK 3·20).
Slice 2 (original outline, for reference)
yt-analytics.readonly (+ re-consent) → YouTubeAnalyticsService behind IChannelStatsProvider
(ai.md): long-form estimatedMinutesWatched (12-mo), Shorts-feed views (90-d), then velocity /
(de)acceleration / ETA from the YppSnapshot history + a sparkline. Own Good Dog landing + task
doc; the once-daily refresh gate and history table are already in place.
Acceptance (slice 1)
- YPP white tab appears below Stream Settings on the Live screen right edge.
- Clicking it slides out the YPP drawer; opening one drawer closes the other; outside-click collapses whichever is open.
- Signed out → "Sign in to YouTube…" hint. Signed in → subs (Tier 1 & Tier 2), uploads/90d, standing-guidance row, manual 2FA/AdSense, education text, refresh + last-updated.
- Every refresh appends a snapshot to SQLite (history ready for slice 2).
- 0 warnings / 0 errors; 313/313 tests pass (one new Good Dog integration test).