fix(ypp): refresh 403'd on every real call — drop the auditDetails part (needs a partner scope)
Creator: 'when I attempt to refresh my YPP page, I get an error about not being able to reach YouTube. Seriously?' Real log: channels.list failed (403) x3. Root cause #1 (the 403): channels.list?mine=true&part=statistics,auditDetails, contentDetails returns 403 insufficientPermissions when the token lacks the youtubepartner-channel-audit scope — which the auditDetails part ALONE requires, per the docs ('A request that retrieves the auditDetails part ... must provide an authorization token that contains the youtubepartner-channel-audit scope'). That scope is MCN partner tooling with a 2-week token-revocation rule; the app must not hold it. TASK-39's 'current scopes suffice, no re-consent' slice-1 claim was wrong for this part; mock-fake tests never touched the real API, so it shipped green and 403'd every refresh since 2026-09-22. https://developers.google.com/youtube/v3/docs/channels/list Fix: part=statistics,contentDetails only; standing flags removed from ChannelStatsService -> YppStatSnapshot surface -> YppTrackerViewModel -> drawer, replaced by an honest deep-link row ('Channel standing isn't exposed to YouTube apps — check the Earn page'). YppSnapshot standing columns stay (schema-stable, always false). channels.list failures now log the response BODY — the bare code could not name insufficientPermissions, which is what made this undiagnosable. Root cause #2 (found by the new Good Dog, masked by the 403): statistics come back as JSON STRINGS ('350'); raw GetInt64() throws. Tolerant ReadInt64 (ValueKind-first; JsonElement.TryGetInt64 THROWS on strings — type-in, not try-type). Good Dog: ChannelStatsServiceTests.CaptureCurrent_RequestsNoAuditDetails_AndStillParsesTheSnapshot (URL asserts no auditDetails + snapshot parses); YppPullOutTests fixture updated. Recipes for both 403/scope and statistics-strings entered in MyMistakes.md. Also shipped in the same commit (shared PreviewPane.xaml + ai.md): the audio-sync status dot removal from the #77 feedback round (creator: 'what is the point of the status light? Lose it') — IntToSyncBrushConverter deleted with it. verify.sh gate: 0 warnings, 316/316 pass, scope-check clean.
This commit is contained in:
+82
-82
@@ -1,102 +1,102 @@
|
||||
# HANDOFF — 2026-09-22 (TASK 42 committed; health-poll + end-race fixes tracked)
|
||||
# HANDOFF — 2026-09-23 (build #77 feedback round; LOG fixes all shipped; app likely running)
|
||||
|
||||
## Branch / Commit State
|
||||
|
||||
`main` HEAD = **`e69db4d`** (TASK 42 top bar) → **`18ab553`** (health-poll fix, committed).
|
||||
Pushed state at `197ee81`. **Working tree DIRTY — end-of-stream race fix (one small change):**
|
||||
`main` HEAD = **`d72949e`** (build-77 top-bar feedback: avatar/gear/cluster). Backlog:
|
||||
`ac6e67a` (end close-out), `18ab553` (health-poll), `e69db4d` (TASK 42), `bb5dcb4` (TASK 41).
|
||||
Pushed state at `197ee81` (36 commits ahead locally — push only at a sub-milestone / on the
|
||||
user's say-so).
|
||||
|
||||
**Working tree DIRTY — two finished hotfix work units from the live build #77 session:**
|
||||
|
||||
```
|
||||
M Services/YouTubeStreamService.cs (EndBroadcastAsync pre-flight)
|
||||
M ytLive.Tests/YouTubeStreamServiceTests.cs (end-close-out test rewritten)
|
||||
M ai.md, Services/index.md, TASKS/task-09-live-stream-management.md, MyMistakes.md
|
||||
M HANDOFF.md (this rewrite)
|
||||
M Controls/PreviewPane.xaml (audio-sync status dot removed + YPP standing row)
|
||||
M Themes/Controls.xaml (IntToSyncBrush converter resource removed)
|
||||
D Helpers/IntToSyncBrushConverter.cs (converter dead — deleted)
|
||||
M Services/ChannelStatsService.cs (YPP auditDetails part dropped; 403 body logged)
|
||||
M Services/YppTrackerViewModel.cs (standing flags removed; honest deep-link text)
|
||||
M ytLive.Tests/YppPullOutTests.cs (fixture loses the standing flags)
|
||||
?? ytLive.Tests/ChannelStatsServiceTests.cs (NEW Good Dog: no auditDetails in URL + parses)
|
||||
M ai.md, TASKS/task-22, TASKS/task-39, TASKS.md, MyMistakes.md, HANDOFF.md
|
||||
```
|
||||
|
||||
## Shipped since last handoff
|
||||
## ⚠️ The app is RUNNING, and that's the story of this round
|
||||
|
||||
- **TASK 42 top bar redesign** `e69db4d` — see below.
|
||||
- **Health-poll parse fix** `18ab553` — `GetStreamHealthAsync` read `healthStatus` as
|
||||
a string; the real API nests `status.healthStatus = {status, lastUpdateTimeSeconds,
|
||||
configurationIssues[]}` (an OBJECT) → every 2026-09-22 session start logged
|
||||
`requires an element of type 'String'`. Fixed to read the nested shape, tolerating the
|
||||
old flat shape; the report-by-exception health banner is alive again.
|
||||
Ac6e67a **could not be verified through the normal gate** because the user was *looking at the
|
||||
running app* (PID 17752) while it happened. `verify.sh` (clean build 0-warnings + full suite +
|
||||
scope check) **cannot run until the app is closed** — an in-place build fails the `apphost.exe`
|
||||
copy MSB3021/MSB3027 while the exe is locked. Compile gate used instead: `dotnet build` reaches
|
||||
the copy stage with XAML/markup compiling clean (only the 2 copy errors → code + XAML are valid).
|
||||
|
||||
## In flight — end-of-stream race fix (end close-out, 2026-09-22)
|
||||
## What the user asked this round (live-launch feedback on #77, all XAML-only)
|
||||
|
||||
`startup.log` showed `Broadcast transition(complete) failed (403) invalidTransition →
|
||||
enableAutoStop will finish` on ALL THREE 2026-09-22 stops (17:09 crash + both test
|
||||
sessions). NOT the old "autoStop already fired" assumption (ai.md had it wrong): the
|
||||
blind `transition(complete)` POST races YouTube/autoStop marking the broadcast complete
|
||||
(via `enableAutoStop=true`, always set — tests included). Fix in the dirty tree:
|
||||
`EndBroadcastAsync` pre-flights `liveBroadcasts.list→status.lifeCycleStatus` and only
|
||||
POSTs complete from `live`/`testing`, skipping silently when already complete
|
||||
(`enableAutoStop` finishes every skip); an inconclusive pre-check still posts (old
|
||||
behavior, backstopped); still never throws. Cite:
|
||||
https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/transition (errors
|
||||
table: invalidTransition = current-status problem, and complete is not gated on
|
||||
streamStatus — only testing/live are).
|
||||
1. **Avatar** much bigger — now 40×40 (was 26), CornerRadius 20, initial 20pt. Shows the streaming
|
||||
account at a glance (113% bigger than the old 26 read).
|
||||
2. **Gear** moved further from the brand — margin 6 → 18 (a couple more "places").
|
||||
3. **Uniform 32-height cluster in the top center** — segments host Border, Start/Go Live, and Test
|
||||
all `Height="32"` so the REC|ON-AIR switch + its actions read as ONE family ("so the user can
|
||||
intuit their purpose"). Running world (reality line) intentionally untouched. **[COMMITTED `d72949e`]**
|
||||
4. **Audio-sync status light LOST** — `"AUDIO SYNC"` slider keeps its numeric tooltip; the green/
|
||||
amber 8×8 dot was "what is the point" dead weight. `IntToSyncBrushConverter` + its resource +
|
||||
TASK-22's "visual feedback" decision all retired with it, both docs updated in-place.
|
||||
5. **YPP refresh bug (this turn, "Seriously?")** — `channels.list?mine=true&part=statistics,
|
||||
auditDetails,contentDetails` 403'd `insufficientPermissions` on EVERY real refresh since slice 1
|
||||
shipped: the **`auditDetails` part alone requires the `youtubepartner-channel-audit` scope**
|
||||
(MCN partner tooling the app must never hold; the "no re-consent / scopes suffice" slice-1 claim
|
||||
was wrong). Fix: part list is now `statistics,contentDetails`; standing flags dropped from
|
||||
snapshot→VM→drawer in favour of an honest "not exposed to apps — check the Earn page" row;
|
||||
`channels.list` failures now log the response body's `error.reason` (the bare code hid this for
|
||||
days); new Good Dog `ChannelStatsServiceTests` guards no-auditDetails + still-parses — which it
|
||||
then used to catch a SECOND latent bug the 403 masked (statistics are JSON strings; `GetInt64`
|
||||
throws → now the tolerant `ReadInt64`, ValueKind-first). Docs:
|
||||
ai.md / TASK-39 / TASKS.md / MyMistakes all corrected. Cite:
|
||||
https://developers.google.com/youtube/v3/docs/channels/list
|
||||
|
||||
## What shipped — TASK 42: top bar redesign (2026-09-22)
|
||||
## What shipped before this round (all committed)
|
||||
|
||||
Full record: **`TASKS/task-42-top-bar-redesign.md`**. Creator directive: "forget this one dog plan
|
||||
bullshit. Our one plan is replacing the top menu bar with something usable." The old bar's six
|
||||
widget grammars (sliding pills, dots, badges, buttons, hidden sign-in context menu, live-window
|
||||
status) collapsed into **ONE surface rendered by the FIRST decision — Record or Stream**:
|
||||
- **`ac6e67a` end-of-stream close-out fix** — every 2026-09-22 stop logged
|
||||
`transition(complete) 403 invalidTransition`; the blind POST raced YouTube/autoStop marking the
|
||||
broadcast complete (`enableAutoStop=true` always set). `EndBroadcastAsync` now pre-flights
|
||||
`liveBroadcasts.list→status.lifeCycleStatus` and skips the POST when already complete/revoked;
|
||||
inconclusive pre-check still posts (backstopped); never throws. Good Dog:
|
||||
`EndBroadcast_Verifies_LifeCycle_Then_Transitions_Complete_Never_Throws` (3 halves: live→GET+POST,
|
||||
complete→skip, 403→error-string). Cite: youtube liveBroadcasts/transition errors table. **Full
|
||||
suite 315/315** (audio-timing flake `Mix_HonorsProviderGains…` cleared on rerun); verify.sh clean
|
||||
pass predates this round's XAML-only edits.
|
||||
- **`18ab553` health-poll fix** — `status.healthStatus` is an OBJECT, not a string; every session
|
||||
start logged `requires an element of type 'String'`. Nested shape parsed (flat tolerated), banner
|
||||
alive again.
|
||||
- **TASK 42** (`e69db4d`) + **TASK 41** (`bb5dcb4`) — one-surface top bar, Test Stream drawer.
|
||||
|
||||
- **Mode switch:** one segmented **REC|ON-AIR** toggle (`SegmentToggle`/`SegmentLabel` styles).
|
||||
Going back = one tap on the other segment. Runs idle-only; retires while running.
|
||||
- **Record world:** switch + **Start Recording** — no YouTube identity at all.
|
||||
- **Stream world:** switch + **Go Live** + Test + account zone (Sign In until connected; then the
|
||||
avatar, right-click Change Account/Logout). **Test is a child of Stream** — procs only ON-AIR-armed
|
||||
AND signed-in (`ShowTestButton`/`CanStartTest`).
|
||||
- **Running:** one reality line `● REC|LIVE|TEST 00:12:34` (green/red/gold `RunningDotBrush`) + End.
|
||||
- **Gear** moved up from bottom bar, ~3 wordmark letters past the brand, one click = Settings / Bug /
|
||||
Feature / About menu (`GearButton_Click`). Bottom-bar gear removed. Task 40 Unit B is DONE by this.
|
||||
- **Sign-in = the account-confirm step:** no Google API enumerates machine accounts; OAuth is
|
||||
single-account, so Switch Account re-runs the chooser (`login_hint`+`select_account`) — login and
|
||||
account-confirmation are one surface. Existing `ChangeAccountCommand`/`LogoutCommand` resurface via
|
||||
the avatar.
|
||||
- **PRIVATE/TEST chips gone from the bar** (dev-phase forced-private becomes an OPTION at ship).
|
||||
## Around the task (carried facts)
|
||||
|
||||
**Good Dog** — `ytLive.Tests/TopBarModeTests.cs` (one `[Fact]`, `RealAppHost.Run`): default record
|
||||
world → arm ON-AIR → sign-in/avatar/Test gating flips → one-tap flip back → direct stream status
|
||||
changing + running reality values. Also folded in: TASK 41's latent **Test pipeline bug fix** —
|
||||
`BeginTestStream` now arms `OnAirPillOn` explicitly (unarmed → encoder booted with zero outputs →
|
||||
"At least one output is required" → forced stop cascade). The TestStreamTests gate updated to match.
|
||||
|
||||
## ⚠️ Around the task
|
||||
- **Full-suite run today: 314/315** (first pass) — the one abort is the pre-existing
|
||||
`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`, which injects
|
||||
REAL physical mouse input (`SetCursorPos` + `mouse_event`, see its own docstring) and no-ops while
|
||||
another window interferes. Cause observed 2026-09-22: **Path of Exile 2 + NVIDIA Overlay were
|
||||
running** (fullscreen-game window hides the test window's rows; the drag never lands → reorder
|
||||
assert fails). `AudioPipelineTests.Mix_HonorsProviderGains…` also flaked one run (timing under
|
||||
load); both cleared on the 315/315 rerun. Close games before running those tests; unrelated to the
|
||||
recent changes.
|
||||
- **Manual verification owed** (needs a real run on Windows): the app is a new build number when
|
||||
next launched — check the new one-surface bar (switch ↔ worlds, gear menu, reality line).
|
||||
- The app may be **RUNNING** — an in-place build fails MSB3021/MSB3027 until closed (temp-OutDir
|
||||
workaround in ai.md). Close it before the manual pass.
|
||||
- Full-suite note: the pre-existing real-camera startup flake did **not** occur in the 315/315 run.
|
||||
- Test-env trap (recorded in the TASK): a dev machine's saved OAuth session loads synchronously and
|
||||
makes `IsConnected` come back true in the VM ctor — signed-in-world tests must force
|
||||
`vm.IsConnected = false/true` explicitly.
|
||||
- `subscriberCount` rounded to 3 sig figs (YPP); carried. YPP slice 2 needs re-consent — do NOT
|
||||
merge with other units.
|
||||
- Polar product `d105dfa1…` still `$99/yr` — must become one-time before launch (carried).
|
||||
`MARCOM.md`/`MONETIZATION.md` gitignored — never commit.
|
||||
- RealMouseDrag test no-ops while a game/fullscreen window steals the mouse (POE 2 seen 2026-09-22)
|
||||
— close games before full-suite runs. AudioPipeline timing flake similar (load-dependent).
|
||||
- Test-env trap: saved OAuth session loads synchronously → force `vm.IsConnected` in signed-in tests.
|
||||
- `subscriberCount` YPP slice 2 needs re-consent — do not merge with other units. Polar `$99/yr`
|
||||
must become one-time before launch. `MARCOM.md`/`MONETIZATION.md` gitignored — never commit.
|
||||
- The **`YppSnapshot.OverallGoodStanding` etc. columns now serialize false forever** (the API path
|
||||
no longer populates them; schema kept stable for the analytics slice). Don't "restore" the
|
||||
standing flags via auditDetails — see the MyMistakes recipe.
|
||||
- TASK 40 App Settings round: units **A (camera) → C (defaults) → D (accent)** remain; A's
|
||||
SharedReadOnly control-write question = only genuine uncertainty.
|
||||
|
||||
## Next step
|
||||
|
||||
Commit this end-of-stream fix (scope-check first, then build 0-warnings + full suite, then ONE
|
||||
commit). Push only on the user's say-so. Then **TASK 40 App Settings round** is queued
|
||||
(`TASKS/task-40-app-settings-round.md`) — units **A (camera) → C (defaults) → D (accent)** remain
|
||||
(B/gear shipped early); Unit A's SharedReadOnly control-write question is the one genuinely
|
||||
uncertain technical point — run its mitigation ladder before a third guess.
|
||||
1. **Close the running app (PID 17752 holds the exe), then run the full gate** —
|
||||
`./scripts/verify.sh` with the full declared scope (audio-dot unit + YPP fix): clean build
|
||||
0-warnings + full suite + scope check. Then commit BOTH units — they share PreviewPane.xaml /
|
||||
ai.md, so a single hotfix commit ("build-77 polish + YPP refresh 403") is the honest shape, or
|
||||
two commits if the user prefers strict splits.
|
||||
|
||||
⚠️ The YPP Good Dog (`ChannelStatsServiceTests`) and the updated `YppPullOutTests` cannot even
|
||||
BUILD until the app closes (the app csproj copy fails on the locked exe) — they were written but
|
||||
not yet executed.
|
||||
|
||||
2. After the gate: push decisions pending (user's call). TASK 40 Unit A next.
|
||||
|
||||
## Critical working rules (unchanged, still binding)
|
||||
|
||||
- **Good Dog = ONE integration test per change.**
|
||||
- **Scope lock:** declare the file list before editing; `./scripts/scope-check.sh` before commit.
|
||||
- WSL builds use the Windows dotnet host (`/mnt/c/Program Files/dotnet/dotnet.exe`, quoted paths).
|
||||
- One runtime model, no model switching, no compaction summaries (user directive).
|
||||
- **Good Dog = ONE integration test per change.** Scope lock + `./scripts/scope-check.sh` before commit.
|
||||
- Windows dotnet host for all WSL builds (`/mnt/c/Program Files/dotnet/dotnet.exe`, quoted paths).
|
||||
- 0 warnings on real clean builds (verify.sh only, never incremental). One runtime model.
|
||||
Reference in New Issue
Block a user