# MyMistakes.md > **Two jobs**, distinguished by heading: > > 1. **Per-task failure log** β€” updated before every commit touching that task: > current iteration + why the last one failed. On task complete, committed AND > pushed β†’ truncate to this stub. A new task does NOT seed this file until its > first failure. > 2. **Recipes registry** (DERIVED-SOLUTION RULE, see `AGENTS.md` πŸ”¬) β€” the durable > home for one-off derived solutions, recipes, and how-tos. The moment you work > out a reusable solution, write it here **in the same session**. GREP THIS FILE > FIRST when you hit a "I've done this before but have to figure it out again" > wall. Recipe entries stay permanently (they are NOT truncated on task > completion) β€” only the failure log truncates. ## πŸ”¬ Recipes registry ### β›” NEVER PRINT CREDENTIALS.md β€” NOT EVEN "FILTERED" (2026-10-09 incident) **What happened:** to look for an API key, a `sed -n '1,40p' CREDENTIALS.md` piped through a redaction regex was run. The regex only matched `password: \`…\``; the **"Sudo pattern" line stores the password inside `echo '…'`**, slipped past the filter, and the full sudo password was printed into the session output (and so into local session logs and the model provider's transcript). Owner had to rotate it. **Rule:** never `cat`/`sed`/`grep`/`read` CREDENTIALS.md (or any secrets file) in a way that can print values. Redaction regexes are not a safety net β€” secrets hide in shapes you didn't anticipate. - **Need a secret for a command?** Load it into a variable, never echo it: `PW=$(sed -n 's/.*password:.*\`\(.*\)\`.*/\1/p' CREDENTIALS.md | head -1)` then use `"$PW"`. - **Need to know what's in the file?** List **keys only**, never values: `grep -oE '^\s*- \*\*[^*]+\*\*' CREDENTIALS.md` (bold labels) β€” or ask the owner. - **Looking for a specific secret?** Test for presence without printing: `grep -qi 'gitea' CREDENTIALS.md && echo present || echo absent`. - Secrets in source (e.g. the Google client secret) live in gitignored `Helpers/OAuthCredentials.cs` β€” check with `git check-ignore -v`, don't open it. **Rotation (YunoHost user `gramps`):** YunoHost owns the password (LDAP) β€” plain `passwd` may not apply. Owner rotates via web admin β†’ Users β†’ gramps, or `sudo yunohost user update gramps -p` (prompts). Never route the new password through the AI. ### YOUTUBE STATISTICS ARE JSON STRINGS; TryGetInt64 THROWS ON STRINGS (RECIPE) YouTube Data API v3 returns `statistics.subscriberCount / viewCount / videoCount` as JSON **strings** (`"350"`), not numbers. (2026-09-23: the YPP parse bug surfaced the moment the auditDetails 403 stopped masking it.) Fix = a tolerant read that checks `ValueKind` FIRST, then `TryGetInt64` for `Number`, then `long.TryParse(GetString())` for `String`. Trap within the fix: **`JsonElement.TryGetInt64` THROWS `InvalidOperationException` on any non-Number token** ("requires an element of type 'Number'") β€” it is try-type-in, not try-catch. Guard on `value.ValueKind` before calling it; a leaked 403β†’parse chain means your "whole request failed" symptom can paper over a second crash that only appears once the 403 is fixed (test the full happy path, not just the error path). ### liveChatId LIVES IN SNIPPET AND ONLY EXISTS ONCE THE BROADCAST IS LIVE (RECIPE) YouTube Data API v3 `liveBroadcasts`. `liveChatId` is in **`snippet.liveChatId`** β€” `contentDetails` has NO such property. AND it is only populated once the broadcast is **live** (the official `GetLiveChatId.java` sample lists `broadcastStatus=active`); a fetch right after insert (lifecycleStatus `ready`) returns nothing. (2026-09-25: this cost a whole investigation β€” the TEST-tab "Chat polling couldn't start β€” Mock Chat Input is disabled" report β€” because the code read `part=contentDetails` + ran before the frame pump pushed RTMP.) Fix = read part=snippet + bounded retry AFTER the encoder starts (`GetBroadcastLiveChatIdAsync(broadcastId, maxAttempts=10, delayMs=2000)`). Debug lens: missing liveChatId β‰ˆ "broadcast not live yet", NOT an auth failure. ### liveChat/MESSAGES.INSERT BODY REQUIRES snippet.type (RECIPE) YouTube Data API v3 `liveChat/messages.insert` rejects the body with `400 MISSING_REQUIRED_FIELD` (`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`) unless the snippet declares `snippet.type` = `textMessageEvent` (or `pollEvent`) alongside `liveChatId` and `textMessageDetails.messageText` β€” the official insert reference lists `type` as a required property. (2026-09-25: the TEST-tab "YouTube rejected the message (error 400)" report β€” the liveChatId fix in TASK 44 had worked and the drawer's Mock Chat Input was issuing a real insert, but the body omitted `type`, and the TASK 41 test only asserted `liveChatId` + `messageText` were present so it stayed green while real YouTube rejected every send. Fix = add `type = "textMessageEvent"` to the body + assert it in the Good Dog test.) Debug lens: MISSING_REQUIRED_FIELD β‰  auth/scope β€” it means the request body shape is wrong, and the false-green test is the classic trap: assertion on the body was about WHAT WE SEND, so YouTube's required fields must be mirrored in the test. ### CHANNELS.LIST auditDetails PART 403s THE WHOLE REQUEST WITHOUT A PARTNER SCOPE (RECIPE) YouTube Data API v3 `channels.list` rejects the ENTIRE request with `403 insufficientPermissions` if your `part=` list includes `auditDetails` but the token lacks `https://www.googleapis.com/auth/youtubepartner-channel-audit` β€” the docs' exact words: "A request that retrieves the auditDetails part for a channel resource must provide an authorization token that contains the youtubepartner-channel-audit scope". That scope is MCN content-partner tooling (with a two-week token-revocation rule); normal-creator apps should NEVER ask for it. 2026-09-23 real-log evidence: YPP refresh 403'd three times in a row while the mock-fake tests stayed green ("current scopes suffice, no re-consent" was wrong). Fixes: (1) drop `auditDetails` from `part=`; (2) log the response BODY β€” the bare status code could not name `insufficientPermissions`, which is what made this failure undiagnosable for days; (3) when a feature needs data no ordinary scope grants, deep-link to the site instead of requesting the partner privilege. ### TRANSITION(COMPLETE) RACES AUTOSTOP: PRE-FLIGHT lifeCycleStatus (RECIPE) A blind `liveBroadcasts.transition?broadcastStatus=complete` POST can 403 `invalidTransition` even though the stream just ended normally β€” 2026-09-22 logged it on EVERY session end. Cause: the broadcast's own state moves toward complete via `enableAutoStop`/YouTube auto-complete; the transition method's errors doc (https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/transition) shows `invalidTransition` = "can't transition from its current status". A blind POST just races β€” read the broadcast's `lifeCycleStatus` first (`liveBroadcasts.list?part=status`) and only POST complete from `live`/`testing`; skip silently otherwise and let `enableAutoStop` finish it. Never throw on the close-out either way. ### YOUTUBE liveStreams LIST: healthStatus IS AN OBJECT, NOT A STRING (RECIPE) YouTube Data API v3 `liveStreams.list` nests health under `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` β€” the `healthStatus` element is an **OBJECT**, and `configurationIssues[]` lives INSIDE it, not directly under `status`. Reading `healthStatus.GetString()` throws System.Text.Json's `The requested operation requires an element of type 'String', but the target element has type 'Object'` β€” the exact log line seen 2026-09-22 on every live health poll (two test sessions). The parse must read `healthStatus["status"]` (and nested `healthStatus["configurationIssues"]`). Something like REST-shape drift is a good reason to grep the API reference (https://developers.google.com/youtube/v3/live/docs/liveStreams) before writing parsers against a "remembered" shape β€” our own flat-string fixture was the wrong assumption the whole time. ### WINRT RESOURCE-ALLOCATION CALLS: WRAP PER-CALL, DEGRADE TO NEXT OPTION (RECIPE) Creator callout (2026-09-15): WinRT/COM calls that allocate or start a resource β€” `InitializeAsync`, `CreateFrameReaderAsync`, `StartAsync`, `CreateReaderAsync` & friends β€” are THROW-HEAVY. An unsupported subtype/format, a device that just vanished, an access mode rejected mid-flight: these surface as `ArgumentException`/`E_INVALIDARG` ("value does not fall within the expected range") or HRESULTs, NOT as a returned status you can switch on. Relying on ONE outer catch to "handle failures" is not handling β€” one rejected call inside a fallback ladder aborts the whole ladder and every untried option. Rule: - Each allocation/start call inside a try/catch of its OWN, so a throw on candidate N falls through to candidate N+1 (log each rejection with its message/status; collect them for the final error string). - `return`/`break` on success must be reached WITHOUT passing through a `finally` that disposes the resource you just committed (classic reader/capture dispose-after-commit bug). - Unsubscribe + dispose the partial resource in the catch block when the subscription happened before the throwing call. - The outer catch stays as the LAST-RESORT net for device-level errors, not the primary one. - Same discipline applies to the frame-consumption side: teardown races reader threads (see HANDOFF crash follow-up) β€” a frame callback can't assume the pipeline is alive. Applied in `MediaCaptureFrameSource`'s reader-subtype ladder (2026-09-15, webcam-take fix). ### "SAVE-RECORDING DIALOG IS CLIPPING CONTENT" β€” WHERE + HOW (RECIPE) **WHERE:** the end-of-recording modal (creator: "when I end a recording the app throws up a save-recording dialog with a default filename") is **`RenameRecordingDialog.xaml` at the repo ROOT** (title "Rename Recording", `x:Class="ytLive.RenameRecordingDialog"`). It is NOT one of the `*Dialog.xaml` files under `Controls/` β€” grep for the window title, not the filename, when the user names a dialog by what it does. Only fix dialogs the user actually named; do not "while I'm here" bump sibling dialogs. **HOW:** the dialog is `ResizeMode="NoResize"` with a fixed `Height` attribute; content client area β‰ˆ `Height βˆ’ ~37px` of window chrome. Sizing/margin changes pushed content down in Z-order until it clipped β€” first the file-name textbox (230 too short), and after the +20% bump (β†’276) the creator found the Cancel/Save button row had ALSO been obscured. Fixed `Height` to 304 (+10% more), `x:Name` the last interactive control (`SaveRecordingButton`) so a test can measure it. **Verify (never eyeball-predict):** `RenameRecordingDialogSizingTests` (RealApp host) instantiates the real dialog, `Show()` + `UpdateLayout()`, then asserts the BOTTOM EDGE of each named control (`TransformToAncestor` against the content root β†’ `TransformBounds(RenderSize).Bottom`) is ≀ the client `ActualHeight`. Every such dialog fix ships with that assertion for every control that was reported obscured. (Client-area math means the button row needs MORE slack than the textbox: its bottom sits deeper because of the `Margin="0,20,0,0"` above it.) ### SPIN GUARD β†’ RESOLVED β€” web overlay transparency + bounding box (RECIPE) **THE ONE ROOT CAUSE THAT EXPLAINS EVERY FAILED TAKE:** WebView2's `CapturePreviewAsync` produces an **OPAQUE** PNG. From the WebView2 spec (sender: MicrosoftEdge/WebView2Feedback `specs/BackgroundColor.md`): "WebView will always honor a webpage's background content." `DefaultBackgroundColor = Transparent` only shows through pages with NO background style β€” the widget's own CSS paints html/body opaque. Every take below built on the false premise "the capture has transparent margins, alpha=0"; it never did. `FindContentBounds` then had no alpha-0 margins to find β†’ wrong crop β†’ black bounding box. The compositor blend saw alpha=255 β†’ black over webcam = "transparency broken". Same bug, three symptoms. **THE FIX (the OBS way, applied 2026-09-08):** inject the transparency BEFORE the page parses using `CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync` β€” documented to run "before the HTML document has been parsed and before any other script included by the HTML document is run" (learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.addscripttoexecuteondocumentcreatedasync). The old `ExecuteScriptAsync` on NavigationStarting/NavigationCompleted ran AFTER page scripts/CSS β†’ widget page repainted background opaque β†’ lost the fight. OBS browser sources do the same via a pre-parse user.css. Kept the nav handlers as a post-load re-assertion. **Take timeline (the honest record):** - `bccdb48` (Aug 28): added FindContentBounds crop β€” correct idea (tight bbox, no dead space), but the capture was OPAQUE so the bbox math was built on nothing. - `5348b5c` (Aug 28, 2 min later): reverted to full-frame no-crop β€” looked "good" for a full-bleed widget, but floating widgets regained dead space ("ghost boundary"). - take-21 (`e002847`) full canvas β†’ shrunken/offset widget (UnifomToFill of whole canvas into a small element = lost resizing). - take-22 (`ed9d7c1`) crop width used as canvas stride for buffer indexing β†’ garbage. - take-23 (`f6802c7`) stride fixed with `src.Width`; PasteKey lacked CropBounds β†’ stale raster cache β†’ stale crop. - take-24 (`081e4c1`) CropBounds in PasteKey β€” STILL BROKEN because the SOURCE ALPHA WAS NEVER REAL. - take-25 (**ME, this session β€” the user's "RE-INTRODUCING THE BOUNDING-BOX PROBLEM"**): I removed FindContentBounds + the crop path entirely, betting full-canvas UniformToFill was the answer. It WASN'T β€” the source is opaque-black, so the element rendered as a SOLID BLACK BOX (the user's screenshot: "a black box in the lower right corner"). Killed the crop β†’ dead space returned AND black box. The compositor math was ALREADY correct; gutting it was vandalism in response to a source-level bug. **Rules, self-inflicted:** 1. Instrument FIRST. This session added: first-capture PNG dump of the raw WebView2 PNG (to `%TEMP%\ytLive-web-.png`) + alpha min/max/mean/%zero + FindContentBounds result logged to startup.log once per session. That's the diff between a five-take loop and a five-minute diagnosis. 2. When the same symptom loops across takes, the PREMISE is wrong, not the code β€” the capture being transparent was the load-bearing premise and it was never verified. 3. Do not delete code paths that fix one axis (crop=bbox) while debugging another (source alpha). Revert scope creep; keep layer contributions separable. **2026-09-10 follow-up β†’ NOW VERIFIED and hardened.** The 12:54 and 13:50 sessions showed the REAL widget document capturing TRANSPARENT (widget dumps `w1..5` for `8d7234ec`: alpha 100% zero, content 12Γ—3; whole-file decodes of `%TEMP%\ytLive-web-*.png` alpha max = 0) while the RECORDING kept showing a black, opaque box over the element rect (crisp edges at the exact rect 1231,679 705Γ—396 β€” NOT the desktop showing through). Concluson: branch (a) β€” the OLD inline `element.style.background='transparent'` injection only wins while the page has nothing to paint; once the widget connects and paints its own container background-COLOR, the capture goes opaque again β†’ black box. The OBS-validated answer (valid for arbitrary pages for a decade) is an injected pre-parse `