Files
LlamaCasty/TASKS/task-39-ypp-journey-tracker.md
T
gramps a9d5f29084 feat(ypp): YPP journey tracker slice 1 — the YPP pull-out below Stream Settings (TASK 39, Good Dog ONE test)
Current-OAuth-scope data, no re-consent: subscriber bars for both tiers (500 fan-funding /
1,000 ad-revenue; the 2027-02-01 doubling is versioned date-aware data in YppThresholds),
3-uploads/90d, live standing from channels.list auditDetails, self-reported 2FA/AdSense
checkboxes + deep links (Google 2-Step / youtube.com/earn), and the "what counts" education.
Every refresh appends a YppSnapshot (SQLite v10) so the analytics slice has history from day one.

- ChannelStatsService (channels.list statistics,auditDetails,contentDetails + playlistItems.list,
  virtual CaptureCurrentAsync = test seam); YppTrackerViewModel mirrors LiveBroadcastFormViewModel.
- MainViewModel.Ypp.cs: Ypp property, ChannelStatsFactoryOverride seam, one-open-at-a-time drawer
  exclusivity (both directions); account sign-in/out/restore hooks in Account.cs.
- PreviewPane.xaml: TextPullOutHost is now a drawer bank [broadcast][ypp][stacked white tabs];
  outside-click collapse covers both drawers. MainWindow.xaml.cs updated.
- Wait - watchers: the whole feature already reviewed against YouTube's own docs (YPP Earn tab +
  API availability) before building — see TASKS/task-39-ypp-journey-tracker.md.
- ONE integration test: YppPullOutTests (snapshot capture/persist + checklist round-trip +
  drawer exclusivity). 313/313 pass, 0 warnings.
- Memory in same commit: TASKS.md row, ai.md Journey-tracker bullet (roadmap→reality), HANDOFF.md.

Slice 2 (Analytics yt-analytics.readonly re-consent → watch-hours/Shorts + velocity/ETA +
sparkline) is queued behind this, not merged.
2026-09-22 08:07:54 -07:00

5.5 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). Standing IS readable from channels.list auditDetails (overall + the three sub-flags) — live, no re-consent.
  • subscriberCount is rounded to 3 significant figures by Google.

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 (YppSnapshot table): 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,auditDetails, contentDetails (subs/views/videos + uploads-playlist id + standing flags) then playlistItems.list (recent 50) counts uploads in the trailing 90 days. Virtual CaptureCurrentAsync = the test seam. No new scope.
  • Services/YppTrackerViewModel.cs — drawer state + Toggle/CloseDrawerCommand + RefreshCommand (mirrors LiveBroadcastFormViewModel), plus: signed-out/loading/error states, tier progress bars (subs Tier-1 & Tier-2, uploads), live standing checkbox + detail, manual 2FA/AdSense checkboxes, WhatCountsText education, deep links, EnsureLoadedAsync (once/day gate) + explicit refresh, OnAccountChanged() hook.
  • ViewModels/MainViewModel.Ypp.cs + ctor — Ypp property, ChannelStatsFactoryOverride test seam (mirrors CameraEnumeratorOverride), 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 in MainViewModel.Account.cs.
  • UI (Controls/PreviewPane.xaml) — TextPullOutHost is 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.cs outside-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.
  • 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 (outline, queued)

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, live standing, 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).