feat(ypp): session report derived from YouTube — no ledger, money off by default (TASK 3·20)

The task asked for a `RewardEvents` table plus a backfill. That would have contradicted the published
promise that no viewer, member or donor information is retained after the session ends, and it would
have meant owning a ledger that YouTube already keeps.

So the report is DERIVED on demand instead:

- `Services/SuperChatEventService.cs` — `superChatEvents.list`, parsed for both event types (a sticker
  must not be dropped), authorised by the existing `auth/youtube` scope, so no re-consent.
- `Models/SessionRewardSummary.cs` — rolls events up for the session window with **per-currency totals
  that are never summed together** (a $5 and a €5 are not $10). Nothing is written down.
- The YPP drawer gains a **THIS SESSION** section: counts are neutral and always shown when there were
  events; money appears only when the creator asks for it. `Show monetary metrics` is a drawer toggle,
  persisted, **off by default** — a creator who never monetized is never shown a $0.00.

Also: money is labelled as estimates (removed/refunded events are excluded and totals can be restated
for weeks, not days).

Tests: `SessionRewardReportTests` — both reward types parse, the window makes it a session rather than
a channel report, currencies stay separate, an empty session shows nothing, a sticker-only session
still counts. `BackOnAirTests` moves into the serialised `MainViewModelLayoutPath` collection: it builds
a MainViewModel directly and was flaking against parallel tests that swap the static layout path.

FAST gate: 557 passed, 0 failed, 0 warnings.
This commit is contained in:
2026-11-05 15:38:32 -08:00
parent 27f655c8de
commit 0fade9702b
13 changed files with 488 additions and 7 deletions
+8 -4
View File
@@ -35,10 +35,14 @@
**Creator rulings:** a free-subscriber mention is a plain chat row — NO sub alert (YouTube
emits no public event for free subs), NO viewer count (vetoed), and every card draws the
"made with LlamaCasty!" brand line (each broadcast = free product placement). **Precursor
(still open): the reward-event persistence half — the canonical `RewardEvents` SQLite table
(all six types persisted per broadcast) + `superChatEvents.list` backfill + the session-report
rollup (TASK 10 → "Related work — monetization awareness"). The shipped alert path parses
events into `ChatMessage` directly in memory; the report reads that table once it exists.**
✅ **RESOLVED 2026-11-05 — derived, not persisted.** The creator's ruling: YouTube is the authority
for monetization, so the app asks it on demand instead of keeping a ledger. `Services/SuperChatEventService.cs`
reads `superChatEvents.list` (existing `auth/youtube` scope — no re-consent), `Models/SessionRewardSummary.cs`
rolls it up for the session window with **per-currency totals that are never summed together**, and the YPP
drawer shows "THIS SESSION" counts. No table, no migration, no backfill, and the published promise that no
viewer/member/donor information is retained stays literally true.
**Money is off by default** ("Show monetary metrics", in the drawer, persisted): counts are neutral, and a
creator who never monetized is never shown a zero. Tests: `SessionRewardReportTests`.**
21. ✅ **Show Desktop toggle** — `Source.ShowDesktop` bool persisted in DB (schema migration + LayoutStore read/write). When checked, primary monitor captures regardless of fullscreen game state. Toggling off falls back to running game or releases to static placeholder. `ClearBackdropCaptureAsync` clears `CaptureKey` + `VideoImageSource` so static fallback renders. Toggle on Live backdrop context menu (IsCheckable MenuItem).
22. ✅ **Top bar — status light + avatar + Log In button** — red/green/pulsing ellipse (disconnected/connected/live). Avatar border visible only when connected, loads via `BitmapImage` code-behind. "Log In" button visible when disconnected, calls `StartStreamCommand` → GoLiveWindow. `ShowStartStream` now requires `IsConnected`.
23. ✅ **Chat preview — one-at-a-time mock messages** — `RunMockChatPreviewAsync`: simple async loop, displays `MockChatMessages[n]` via `Take(n+1)`, 1000ms between messages, wraps at end. `ChatPreviewEnabled` property on Source (default true). Stops on real messages, restarts on fade timer clear.