feat(stream): overlay expansion — evolution & batch shows, QR codes, control overhaul #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/stream-batch-overlay"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Combines the full stream overlay expansion (previously stacked PRs #2–#6) into one PR: the 6,138-line OBS overlay and the control page are decomposed into modules, and the overlay learns evolution ceremonies, adoption elimination shows, QR codes, a settings tab with a croppable spread layout, and a polished lobby.
Refactors
components/stream-control/obs/—ObsOverlayClientorchestrator,useObsSession, pure unit-testedcommandPolicy, and scene modules (spin board, lobby/countdown, BRB, test card, evolution, batch)components/stream-control/control/— tabbedControlShell(Spin / Evolution / Batch / Settings) with a persistent status bar + OBS preview, panels wired throughStreamControlContextEvolution on stream
/evolution/[slug]Adoption elimination show
/adoption/[slug]session.batchState(apiKey-authenticated overlay reports, seq-guarded) — overlay reloads resume mid-showQR codes
attachViewSlug), overlay fades in a scannable QR card to/view/[slug]attachBatchSlug) and evolution (slug travels in the command)Settings tab + spread layout
useFollowGuardprotects fresh local edits from in-flight echoes)mergeSettingsmutation: server-side settings merge so concurrent per-field syncs can't clobber each otherLobby
Testing
/adoption/[slug]), spread test card, highlight buttons (reset/persist semantics), two-tab control syncSomewhere a GPU is overheating so I don't have to think.
Issue: batch reload can ask for an extra cull after a cull lands during reload
File/line:
frontend/components/stream-control/obs/scenes/BatchScene.tsx:255seenEliminationsRefis initialized from the currenteliminatedIds.length. That works for a normal mount, but it breaks the resume case this code is trying to protect: if the OBS overlay reloads aftercullBatchCathas patchedeliminatedIdsandawaitingCull: false, but before the previous overlay instance advancesstageIndexand reports the next stage, the new component mounts withstageIndexstill pointing at the just-culled reveal andeliminatedIds.lengthalready incremented.Because the ref starts at the incremented length, the cull reaction effect treats that elimination as already processed and never runs the
setStageIndex(stageIndex + 1)path. The reveal driver then re-renders the same stage with one fewer cat and, after the hold, reportsawaitingCull: truefor that same stage. In practice that can make the streamer cull twice for one reveal and skip a later reveal stage.The resume logic should derive the local stage from the persisted elimination count, or initialize the "seen" count from the number of eliminations already reflected by
stageIndex, not from the raw currenteliminatedIds.length. For example, on mount/resume, ifliveState.awaitingCullis false andeliminatedIds.length > stageIndex, advance localstageIndexto the elimination count before rendering/reporting another wait.Issue: nested buttons in the cull board create invalid DOM and unreliable clicks
File/line:
frontend/components/stream-control/control/BatchCullBoard.tsx:243Each cat card is rendered as a
<button>, and then the spotlight/potential/favourite controls are rendered as additional<button>elements inside that button. HTML does not allow interactive content inside a button. React will warn about the invalid nesting, and browsers are allowed to repair the DOM by implicitly closing the outer button, which can make the layout and event handling differ from what the JSX suggests.event.stopPropagation()on the corner controls does not fix the invalid DOM. It only helps if the browser kept the tree in the shape React expected. This is especially risky here because the outer button controls mark/cull behavior while the inner buttons control highlight state; a browser repair or hydration mismatch can make a corner click mark/cull the cat or make the corner controls unreliable.A safer structure is to make the card wrapper a non-interactive
divand put the mark/cull button plus the three corner buttons as siblings inside it, or keep the main card button and render the corner action buttons as absolutely positioned siblings outside the main button.Issue: saved Evolution settings reject every wild clan on restore/sync
File/line:
frontend/components/stream-control/control/EvolutionPanel.tsx:116The UI allows selecting both controlled and wild clans because
CLAN_ORDERincludes...CONTROLLED_ARCHETYPESand...WILD_ARCHETYPES, and the sync effect writesselectedClanstoevolutionInfounchanged. The restore/follow effect, however, validates stored clans withCONTROLLED_ARCHETYPESonly:That means any persisted selection containing
volt,crystal,void, orsteelfails theeverycheck and the whole stored clan list is ignored. A reload or second open control tab will fall back to the previous local/default clan selection even though the session contains the user's saved wild-clan selection, so the control UI and lobby preview can diverge from the actual saved settings.Use the full archetype set for this guard, for example
isEvolutionArchetypefromevolutionGenerator, or validate againstCLAN_ORDER/[...CONTROLLED_ARCHETYPES, ...WILD_ARCHETYPES].Issue: failed finalist save says it will retry, but nothing schedules a retry
File/line:
frontend/components/stream-control/control/BatchPanel.tsx:312When the finalist save fails, the catch block resets
persistedSeqRef.current = nulland shows "Failed to save the litter — will retry." The effect only runs when one of its dependencies changes, though. At the terminal state all culls are already done,batchLiveStateandcurrentBatchCommandmay not change again, and resetting a ref does not trigger a render. So after a transient failure increateMapper,createBatch, orattachBatchSlug, the promised retry usually never happens and the overlay remains stuck in the completed-but-unsaved state without a QR slug.This needs an explicit retry trigger: for example a retry counter state/backoff timer, or a visible manual retry action that increments state and reruns the persistence effect. Otherwise the only accidental retry path is some unrelated dependency changing, such as a settings or creator-name update.
All four review findings addressed in
125129b:BatchScene reload-after-cull race — fixed.
seenEliminationsRefis now derived from the resumed stage (Math.min(stageIndex, maxEliminations)) instead ofeliminatedIds.length. Each of the firstmaxEliminationsstages requires exactly one cull to advance, so a cull that landed right before a reload (eliminated, stage not yet advanced) is detected as unprocessed and advances the stage instead of triggering a second cull.Nested buttons — this one is a false positive on the DOM structure: the corner controls are absolutely-positioned siblings of the card
<button>inside the relative wrapper<div>, not children of it (BatchCullBoard.tsx:242–338), so the DOM is valid and React emits no nesting warning. The misleadingstopPropagation()calls (dead code — sibling clicks never reach the card handlers) and the comment implying nesting are removed.Wild clans rejected on restore — fixed. The follow effect now validates stored clans against
CLAN_ORDER(controlled + wild) instead ofCONTROLLED_ARCHETYPES, matching what the UI lets you select and what the sync effect writes.Phantom retry on failed finalist save — fixed. The catch block now schedules a real retry: a 5 s timer bumps a
persistRetryTickstate that is a dependency of the persist effect, so the save re-runs even though the show is in a terminal state. The timer is cleaned up on effect re-run, andpersistedSeqRefstill prevents double saves.Verified: typecheck, biome, and all 217 vitest tests pass.
Rechecked after
125129b8a8bf0a72eab10704165f56afeb8bbd80.The four review findings are addressed:
seenEliminationsRefis now based on the resumed stage rather than the current eliminated count.CLAN_ORDER, covering controlled and wild clans.persistRetryTick.Verification passed locally:
pnpm run typecheck,pnpm test -- --run,pnpm run lint(warning-only), andpnpm run build.Both follow-up findings fixed in
3419c5c:[P1] Stale batch stage reports —
reportBatchStagenow rejects regressions before patching:stageIndexthan the stored one is ignored (monotonic stages per seq);awaitingCull: trueis ignored once that stage's cull has already landed (eliminatedIds.length > stageIndex— each of the firstmaxEliminationsstages takes exactly one cull), so the control board can never be re-prompted to cull from an old stage;awaitingCull → false: the only legitimate way out of the awaiting state iscullBatchCatitself.This also composes with the earlier resume fix: a reloaded overlay that briefly re-renders an already-culled stage now has its transient
awaitingCullreport suppressed server-side as well.[P2] Spin QR lost on early wheel —
attachViewSlugnow also accepts the wheel command derived from the same spin (type === "wheel",seq === spinSeq + 1,lastWheelSpinForSeq === spinSeq) and stamps the slug onto it. The overlay readscurrentCommand.viewSlugtype-agnostically and slug patches don't bumpseq, so the QR fades in during the wheel reveal without a re-dispatch. Attaching to an unrelated newer command is still refused.Typecheck, biome, and all 217 tests pass; functions deployed to the dev Convex instance.