# HANDOFF — 2026-09-26 (TASK 47 shipped, NOT yet pushed: alert box video) ## Branch / Commit State `main` — TASK 47 changes are staged-ready, **uncommitted** in the working tree (repo was clean at `2c9af94`, the TASK 46 handoff commit; `origin/main` = `ecb329e`). Commit + push pending the creator's go / milestone signal. The TASK 46 commit was pushed 2026-09-25 (`origin/main` = `197ee81..ecb329e`). ## ⚠️ The unit that just shipped (creator directive) TASK 43 closed with "make the alerts animate like a video" / "I want to play my own video for alerts" — that follow-up is **TASK 47, done 2026-09-26**: - **Video alerts:** the alert box plays a clip on every alert — the shipped built-in (`Assets/alert-default.mp4`, stamped into the `Asset` table at startup via `StampDefaultAlertVideo`, id tracked in the `AlertDefaultVideoAssetId` setting) unless the creator picks their own file (path-only reference, **never stored in the DB**; the six `AlertRenderer` animations remain the fallback when nothing plays). Per-alert `IAlertClipDecoder` (ffmpeg bgra + f32le pipes, real-time paced, disposed at drain). - **Read-time fade** (`FadeDurationSeconds = 0.30` in `AlertOverlayLayer`): fade-in alpha on a straight-alpha frame copy; EOF → freeze-frame → fade-out → drain back to idle/transparent. - **Message ticker top-of-frame** ("Funder — Super Chat · $10.00"): `AlertTickerRenderer` (marquee 140px/s), **never baked** — threaded via a `FramePump._alertTicker` seam into `SceneCompositor.Render/CompositeLayers` and mixed into `BuildFullRenderSignature`. - **Alert audio at unity, NO duck** (creator ruling): `AudioMixer.EnqueueAlertAudio` + an 8s stereo-48k ring drained pre-limiter; per-alert `Volume` slider instead. - New Stream Alerts config section in LeftPanel (built-in pill, path + Browse…, Reset, ticker pill, volume). **Good Dog test:** `ytLive.Tests/AlertLayerVideoTests.cs` (RealApp STA) — one `[Fact]` driving a **fake** `IAlertClipDecoder` through the whole lifecycle: custom path wins per-source, decoder spawns/disposes (Start=1, Dispose=1 at drain), alpha envelope 0→127→255, pre-fade audio dropped while ramp audio forwards at volume×fade (0.8×0.5→[0.2,−0.1]) and full-fade at volume only, ticker renders non-null, has pixels and **scrolls** between ticks (and null when idle), EOF freeze-fades to a null frame. The test caught a real bug on the first run: the clip branch of `Advance` never cleared `_current` before `AdvanceToNext` → the layer stayed `IsPlaying` after drain (see `MyMistakes.md`). **Gate: clean build 0 warnings; alert tests 2/2; full vstest 318/319** — the one failure `LayerReorderPersistenceTests.RealMouseDrag…` is the HANDOFF-documented env class (physical mouse drag no-ops when the desktop/session isn't interactive; this unit it failed 2× then passed in isolation on re-run; SourceList row geometry provably unchanged — `AlertOverlayLayer` — and its two sibling reorder tests, the same persistence path, pass). ## Follow-ups queued (NOT done in this unit) - **TASK 3 item 20 persistence half** — canonical `RewardEvents` SQLite table + `superChatEvents.list` (30-day) backfill + session-report rollup (still in-memory only). - TASK 3 item 16 (Text source); TASK 40 units A/C/D; TASK 32–36; TASK 12 master limiter — all queued unchanged. ## Around the task (carried facts) - RealMouseDrag tests no-op while a game/fullscreen window steals the mouse (POE 2 seen 2026-09-22) — **close fullscreen windows before full-suite runs**; the single test passes in isolation when re-run alone. AudioPipeline timing flake similar. - 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. `$99/yr` Polar must become one-time before launch. `MARCOM.md`/`MONETIZATION.md` gitignored — never commit. - Latent bug the YPP 403 masked: statistics are JSON **strings** — always the ValueKind-first `ReadInt64` (MyMistakes recipe), never `GetInt64`. - **API facts (TASK 44 + 45):** a liveChatId fetch before the broadcast is live returns nothing by design (read `snippet.liveChatId`, poll until active); `400 MISSING_REQUIRED_FIELD` from `liveChat/messages.insert` = wrong body shape (missing `snippet.type`), NOT auth/scope. - **WPF test technique (TASK 46):** a `MouseButtonEventArgs { RoutedEvent = Mouse.PreviewMouseLeftButtonDownEvent }` raised on the window root deterministically simulates an "outside the drawer rail" click — InputBindings are NOT triggered by `RaiseEvent`. - **TASK 47 sticky facts:** bgra from ffmpeg is opaque (alpha=255); audio pipe = carry-buffer loop (PCM bytes → float chunks straddle pipe reads); default clip lives at `%TEMP%\ytLive-alert-{id}.mp4` when materialized; decoder per-play, never the refcounted `MediaVideoSourceManager`. Recipe in `MyMistakes.md`. DB stays `user_version 10` — TASK 47's Source columns are additive guarded ALTERs. - Every committed change needs a **close + relaunch** of the running app to be seen. ## Next step 1. Run `scripts/scope-check.sh` with the declared file list (TASK 47), final clean build (verify, 0 warnings, `"/mnt/c/Program Files/dotnet/dotnet.exe" build ytLive.csproj`) + full vstest. 2. Single commit (code + this unit's docs: `ai.md`, `TASKS.md`, `TASKS/task-47-alert-videos.md`, `Services/index.md`, `HANDOFF.md`, `MyMistakes.md`). Push at the creator's go/milestone. 3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next. ## Critical working rules (unchanged, still binding) - **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 only, never incremental). One runtime model. - No menus/polls with the creator; decisions asked once then held; do the work, then report.