feat(stream): overlay expansion — evolution & batch shows, QR codes, control overhaul #7

Merged
beasty merged 15 commits from feat/stream-batch-overlay into main 2026-06-12 15:06:16 +00:00
Owner

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

  • OBS overlay decomposed into components/stream-control/obs/ObsOverlayClient orchestrator, useObsSession, pure unit-tested commandPolicy, and scene modules (spin board, lobby/countdown, BRB, test card, evolution, batch)
  • Control page decomposed into components/stream-control/control/ — tabbed ControlShell (Spin / Evolution / Batch / Settings) with a persistent status bar + OBS preview, panels wired through StreamControlContext

Evolution on stream

  • Evolution tab: clans, target level, charge length (8/15/30/60 s), starter (mystery egg or saved cat), title — generates + persists on the authed control page, overlay receives params-only data and re-renders locally
  • Full ceremony on the overlay (hands-off mode), then a dynamic lineage board that maximises sprite size, plus a QR code to /evolution/[slug]
  • Pokémon-style charge: contenders keep flying in from random screen edges for the whole charge and hover around the ceremony point; the real final sprite sneaks in early at a random size, rises to the top with a glow on selection, and every failed contender tumbles off the bottom with gravity

Adoption elimination show

  • The adoption generator's true elimination format: a pool of 10 + stage-count cats, one parameter revealed per stage, streamer culls one cat per stage from a full-width live board until 10 finalists remain; finalists are saved as a regular adoption batch with a QR to /adoption/[slug]
  • Live sync through session.batchState (apiKey-authenticated overlay reports, seq-guarded) — overlay reloads resume mid-show
  • Cull board: single click marks a cat red for viewers, double click removes; round corner buttons per card — spotlight (shows the cat big on stream + control), yellow "potential" (resets each cull), favourite heart (persists all show)
  • Grid packing maximises sprite size for the survivor count on every stage; sprites keep the previous stage's render until the next one is ready (no flicker)
  • Batch tab has its own palette selector, shown on the lobby card

QR codes

  • Spin results: slug attached to the in-flight command without a seq bump (attachViewSlug), overlay fades in a scannable QR card to /view/[slug]
  • Same pattern for batch (attachBatchSlug) and evolution (slug travels in the command)

Settings tab + spread layout

  • OBS layout (default / spread), background colour + opacity, lobby animation + BRB preset moved in from the old Scenes tab
  • Spread mode gives every overlay element its own region with 50 px gutters on a 2120×1180 canvas; test mode renders a labelled guide with exact crop coordinates per region
  • Control page state follows the session live — multiple open control tabs stay in sync (useFollowGuard protects fresh local edits from in-flight echoes)
  • mergeSettings mutation: server-side settings merge so concurrent per-field syncs can't clobber each other

Lobby

  • Mode-aware info card (spin settings / evolution preview incl. estimated length / batch preview incl. its palettes)
  • Three new animations (parade, orbit, bubbles); parade cats march in their own lanes at their own pace
  • Switching animation styles glides existing cats into the new pattern instead of fading them out

Testing

  • 217 vitest tests pass (incl. new commandPolicy, streamWheel, streamBatch suites); typecheck + biome clean
  • Full Playwright passes: spin/wheel/lobby/BRB regression matrix, evolution ceremony E2E, complete 23-cull elimination show E2E (mark → cull → save → QR → /adoption/[slug]), spread test card, highlight buttons (reset/persist semantics), two-tab control sync

Somewhere a GPU is overheating so I don't have to think.

## 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 - **OBS overlay decomposed** into `components/stream-control/obs/` — `ObsOverlayClient` orchestrator, `useObsSession`, pure unit-tested `commandPolicy`, and scene modules (spin board, lobby/countdown, BRB, test card, evolution, batch) - **Control page decomposed** into `components/stream-control/control/` — tabbed `ControlShell` (Spin / Evolution / Batch / Settings) with a persistent status bar + OBS preview, panels wired through `StreamControlContext` ### Evolution on stream - Evolution tab: clans, target level, charge length (8/15/30/60 s), starter (mystery egg or saved cat), title — generates + persists on the authed control page, overlay receives params-only data and re-renders locally - Full ceremony on the overlay (hands-off mode), then a dynamic lineage board that maximises sprite size, plus a QR code to `/evolution/[slug]` - Pokémon-style charge: contenders keep flying in from random screen edges for the whole charge and hover around the ceremony point; the real final sprite sneaks in early at a random size, rises to the top with a glow on selection, and every failed contender tumbles off the bottom with gravity ### Adoption elimination show - The adoption generator's true elimination format: a pool of 10 + stage-count cats, one parameter revealed per stage, streamer culls one cat per stage from a full-width live board until 10 finalists remain; finalists are saved as a regular adoption batch with a QR to `/adoption/[slug]` - Live sync through `session.batchState` (apiKey-authenticated overlay reports, seq-guarded) — overlay reloads resume mid-show - Cull board: single click marks a cat red for viewers, double click removes; round corner buttons per card — spotlight (shows the cat big on stream + control), yellow "potential" (resets each cull), favourite heart (persists all show) - Grid packing maximises sprite size for the survivor count on every stage; sprites keep the previous stage's render until the next one is ready (no flicker) - Batch tab has its own palette selector, shown on the lobby card ### QR codes - Spin results: slug attached to the in-flight command without a seq bump (`attachViewSlug`), overlay fades in a scannable QR card to `/view/[slug]` - Same pattern for batch (`attachBatchSlug`) and evolution (slug travels in the command) ### Settings tab + spread layout - OBS layout (default / spread), background colour + opacity, lobby animation + BRB preset moved in from the old Scenes tab - Spread mode gives every overlay element its own region with 50 px gutters on a 2120×1180 canvas; test mode renders a labelled guide with exact crop coordinates per region - Control page state follows the session live — multiple open control tabs stay in sync (`useFollowGuard` protects fresh local edits from in-flight echoes) - `mergeSettings` mutation: server-side settings merge so concurrent per-field syncs can't clobber each other ### Lobby - Mode-aware info card (spin settings / evolution preview incl. estimated length / batch preview incl. its palettes) - Three new animations (parade, orbit, bubbles); parade cats march in their own lanes at their own pace - Switching animation styles glides existing cats into the new pattern instead of fading them out ## Testing - 217 vitest tests pass (incl. new commandPolicy, streamWheel, streamBatch suites); typecheck + biome clean - Full Playwright passes: spin/wheel/lobby/BRB regression matrix, evolution ceremony E2E, complete 23-cull elimination show E2E (mark → cull → save → QR → `/adoption/[slug]`), spread test card, highlight buttons (reset/persist semantics), two-tab control sync Somewhere a GPU is overheating so I don't have to think.
Split the 6,138-line OBSSpinClient into components/stream-control/obs/:

- ObsOverlayClient.tsx — orchestrator with the spin engine and command
  dispatch (the engine runs across scene changes, so it stays together)
- spinSupport.ts — module-level types, constants, and pure helpers
- useObsSession.ts — session subscription + settings derivation
- commandPolicy.ts — pure dispatch decisions (seq guard, reload restore,
  staleness) with unit tests
- scenes/ — TestCard, BrbScene, LobbyCountdownScene, SpinBoard as
  presentational components

No behavior change; all code moved verbatim apart from prop wiring.

Powered by human calories and mass GPU cycles.
Split the 1,862-line StreamControlClient into components/stream-control/control/:

- ControlShell.tsx — owns all state, Convex wiring, and command handlers
  (moved verbatim); renders StatusBar + tab strip + persistent OBS preview
- context.ts — StreamControlContext so panels consume shell state without
  30-prop drilling
- StatusBar.tsx — scene buttons (Lobby/BRB/Test/Clear) + Copy OBS URL,
  always visible above the tabs
- SpinPanel.tsx — spin/wheel triggers, generator settings, share/history
- ScenesPanel.tsx — lobby animation settings + BRB preset
- PreviewCard.tsx — sticky OBS preview next to the active tab
- helpers.ts / controls.tsx / SettingsCode.tsx / BrbPresetSection.tsx —
  moved verbatim

Panels stay mounted on tab switch so drafts and sliders keep their state.
Also prunes unused imports in obs/spinSupport.ts (biome).

Powered by human calories and mass GPU cycles.
When a stream spin's history save completes, the control page stamps the
saved share slug onto the in-flight command (attachViewSlug — same seq, so
the overlay does not re-dispatch) and the overlay fades in a scannable QR
code linking to /view/[slug] next to the finished cat.

- schema: optional viewSlug on cat_stream_sessions.currentCommand
- catStream: triggerSpin returns its seq; new attachViewSlug mutation with
  a race guard (only stamps the matching spin command)
- streamWheel: wheel commands carry viewSlug forward so the QR survives
  the wheel reveal (+ tests)
- obs/QrBadge: solid light card (quiet zone via padding) with qrcode.react
- SpinBoard: shows the badge bottom-left of the cat zone once spinDone;
  survives overlay reloads since the slug lives on the persisted command

Powered by human calories and mass GPU cycles.
Adds the evolution generator to the stream: configure clans, target level,
spin time, and starter on the control page's new Evolution tab; the OBS
overlay plays the full ceremony hands-off, then shows a compact lineage
board with a QR code to the saved /evolution/[slug] page.

Control page (EvolutionPanel) generates and persists the batch (authed
surface), then sends params-only cat data — the overlay re-renders sprites
locally, keeping the session document far under Convex's 1 MiB cap.

- schema/catStream: evolutionCommandValidator + "evolution" command type +
  triggerEvolution mutation
- EvolutionCeremony: hideControls prop — hides speed/skip/advance, ignores
  taps, auto-finishes after the finale
- obs/scenes/EvolutionScene: progressive sprite rendering feeding the
  ceremony, teaser frames, lineage board finale with themed branch columns
  + QrBadge; stale reloads (older than the estimated ceremony) jump
  straight to the lineage
- extracted shared lib modules so the generator page and stream panel use
  one implementation: persistEvolutionBatch, buildRandomEvolutionStarter,
  ceremonyEstimate, streamEvolution (command shape + staleness estimate)

Powered by human calories and mass GPU cycles.
Adds the batch (litter) generator to the stream. The control page's new
Batch tab rolls N cats with the Spin tab's settings, saves them as an
adoption batch (per-cat profiles + /adoption/[slug]), and the OBS overlay
reveals them card by card with a confetti burst each, finishing with a QR
code to the saved litter.

- schema/catStream: batchCommandValidator + "batch" command type +
  triggerBatch mutation (params-only payload, overlay re-renders locally)
- control/BatchPanel: cat count + title; generation reuses the spin
  settings and persistence path
- obs/scenes/BatchScene: staggered card pops driven by render progress,
  final grid + QrBadge; stale reloads jump straight to the grid
- lib/adoption/streamBatch: shared command shape + reveal-duration
  estimate
- style: lobby settings card now uses the same amber/zinc gradient
  treatment as the spin board and reveal scenes

Powered by human calories and mass GPU cycles.
- Forgejo hosting + fj CLI for PRs (gh does not work)
- pnpm store-version mismatch workaround
- Convex: .js relative imports, generated-API leak from temp files,
  CLI data/run commands
- Stream overlay rules: params-only commands (1 MiB session doc cap),
  control page owns generation/persistence, no-seq-bump patches

Powered by human calories and mass GPU cycles.
Reworks the batch overlay into the adoption generator's real format and
folds in a round of streamer-experience feedback:

Elimination show (replaces the simple reveal):
- pool of 10 + stage-count cats, each reveal stage finishes every survivor
  one step further (shared partial-render lib with unit tests); streamer
  culls one cat per stage until ten finalists remain, which are then saved
  as a normal adoption batch with the QR on the overlay
- progress lives in session.batchState (overlay reports stages via a new
  apiKey mutation; control culls into it) so overlay reloads resume mid-show
- single click marks a cat (viewers see a red pulse on stream),
  double-click removes it; no red banner on the overlay, bigger labels
- full-width live cull board below the preview with big sprites, visible
  on every tab

Control page:
- Scenes tab becomes a Settings tab: overlay layout (BeastyPage / spread
  for OBS cropping), background colour + opacity (chroma-key friendly),
  lobby animation, BRB preset
- new mergeSettings mutation: per-field syncs merge server-side so
  concurrent writers can't clobber each other's settings fields

Overlay:
- lobby info card follows the selected mode (spin settings / evolution
  ceremony with clans / adoption elimination) instead of always showing
  single-cat-plus info
- three new lobby animations: parade, orbit, bubbles
- spread layout + background settings applied from the session
- evolution charge phase: contender sprites fly into the ceremony point
  and get rejected Pokémon-style until the real form claims the spot

Powered by human calories and mass GPU cycles.
A cull arriving while the batch scene was mid-render (e.g. right after an
overlay reload, when the server still reported awaitingCull) was marked as
seen without advancing the stage, permanently stalling the show. Only mark
eliminations as seen once the scene is actually in the awaiting phase, so
the pending cull is processed when it re-enters it.

Powered by human calories and mass GPU cycles.
Ceremony charge phase, round two of feedback:
- ~10x more contenders (spawn every 320ms, varied sizes 170-300px and
  flight lengths ~6-9s) flying in slowly from the actual screen edges via
  a full-viewport portal; they are fully visible from the moment they
  cross the edge instead of fading in near the centre
- rejected contenders get a proper fling: stronger tumble, squash, and an
  easeIn eject
- near the end of the charge one contender is "chosen": it lands on the
  ceremony point, grows, and pulses with the clan's glow until the flash
  replaces it with the real form
- teaser frame budget raised (up to 60 distinct styles per charge)

Lineage board now sizes itself to the lineage shape: a single evolution
level renders as a flat grid of big themed cards (no more skinny columns
with tiny sprites at 10 clans x level 1); deeper lineages compute sprite
size from both branch count and level count to fill the board.

Powered by human calories and mass GPU cycles.
Spawn every 1.8s with ~19-26s flights — roughly ten unhurried cats in
the air at once instead of twenty fast ones.

Powered by human calories and mass GPU cycles.
Contenders now fly in from random screen edges at a uniform pace with
independently random sizes, hover around the ceremony point, and the
real final sprite departs on a timed schedule so it lands exactly as
the charge ends — rendered above the swarm with a glow. When it claims
the spot, every failed contender tumbles off the bottom of the screen
with scattered drift and rotation instead of individual rejects.

Powered by human calories and mass GPU cycles.
Contenders now keep flying in for the entire charge at a 50% higher
rate (one every 1.2s, count scales with charge length), so late
arrivals are still mid-flight when the selection happens and fall with
the rest. The real final form departs at a random moment between 1 and
12 seconds and hovers anonymously among the crowd at a normal random
size — only on selection does it rise to the top of the swarm, move to
the exact ceremony point, and grow and glow. Stream charge presets are
now 8/15/30/60 seconds (default 15) and the hover ring is wider to fit
the bigger crowd.

Powered by human calories and mass GPU cycles.
Control page: settings now follow the session live, so multiple open
control tabs stay in sync; a follow-guard makes sure fresh local edits
are never reverted by in-flight echoes. The Batch tab gets its own
palette selector (shown on the lobby card too) instead of inheriting
the Spin tab's palettes.

Spread layout: every overlay element now gets its own region with 50px
gutters on a 2120x1180 canvas (cat canvas, param board, QR, wheel,
wheel banner, layer bar), and test mode shows a labelled guide with
each region's exact crop coordinates for OBS.

Batch elimination: cards keep the previous stage's sprite until the
next render is ready (no more flicker after culls), the grid packs
itself to maximise sprite size for the survivor count, and each cull
board card gets round corner buttons — top-left spotlights the cat big
on stream and the control board, top-right marks a yellow "potential"
(resets each cull), bottom-right a favourite heart that persists for
the whole show. Overlay mirrors all three.

Lobby: parade cats march in their own lanes at their own pace instead
of clumping, and switching animation styles glides the existing cats
into the new pattern instead of fading them out.

Powered by human calories and mass GPU cycles.
Author
Owner

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:255

seenEliminationsRef is initialized from the current eliminatedIds.length. That works for a normal mount, but it breaks the resume case this code is trying to protect: if the OBS overlay reloads after cullBatchCat has patched eliminatedIds and awaitingCull: false, but before the previous overlay instance advances stageIndex and reports the next stage, the new component mounts with stageIndex still pointing at the just-culled reveal and eliminatedIds.length already 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, reports awaitingCull: true for 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 current eliminatedIds.length. For example, on mount/resume, if liveState.awaitingCull is false and eliminatedIds.length > stageIndex, advance local stageIndex to the elimination count before rendering/reporting another wait.

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:255` `seenEliminationsRef` is initialized from the current `eliminatedIds.length`. That works for a normal mount, but it breaks the resume case this code is trying to protect: if the OBS overlay reloads after `cullBatchCat` has patched `eliminatedIds` and `awaitingCull: false`, but before the previous overlay instance advances `stageIndex` and reports the next stage, the new component mounts with `stageIndex` still pointing at the just-culled reveal and `eliminatedIds.length` already 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, reports `awaitingCull: true` for 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 current `eliminatedIds.length`. For example, on mount/resume, if `liveState.awaitingCull` is false and `eliminatedIds.length > stageIndex`, advance local `stageIndex` to the elimination count before rendering/reporting another wait.
Author
Owner

Issue: nested buttons in the cull board create invalid DOM and unreliable clicks

File/line: frontend/components/stream-control/control/BatchCullBoard.tsx:243

Each 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 div and 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: nested buttons in the cull board create invalid DOM and unreliable clicks File/line: `frontend/components/stream-control/control/BatchCullBoard.tsx:243` Each 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 `div` and 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.
Author
Owner

Issue: saved Evolution settings reject every wild clan on restore/sync

File/line: frontend/components/stream-control/control/EvolutionPanel.tsx:116

The UI allows selecting both controlled and wild clans because CLAN_ORDER includes ...CONTROLLED_ARCHETYPES and ...WILD_ARCHETYPES, and the sync effect writes selectedClans to evolutionInfo unchanged. The restore/follow effect, however, validates stored clans with CONTROLLED_ARCHETYPES only:

info.clans.every((clan) =>
  (CONTROLLED_ARCHETYPES as readonly string[]).includes(clan as string),
)

That means any persisted selection containing volt, crystal, void, or steel fails the every check 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 isEvolutionArchetype from evolutionGenerator, or validate against CLAN_ORDER/[...CONTROLLED_ARCHETYPES, ...WILD_ARCHETYPES].

Issue: saved Evolution settings reject every wild clan on restore/sync File/line: `frontend/components/stream-control/control/EvolutionPanel.tsx:116` The UI allows selecting both controlled and wild clans because `CLAN_ORDER` includes `...CONTROLLED_ARCHETYPES` and `...WILD_ARCHETYPES`, and the sync effect writes `selectedClans` to `evolutionInfo` unchanged. The restore/follow effect, however, validates stored clans with `CONTROLLED_ARCHETYPES` only: ```ts info.clans.every((clan) => (CONTROLLED_ARCHETYPES as readonly string[]).includes(clan as string), ) ``` That means any persisted selection containing `volt`, `crystal`, `void`, or `steel` fails the `every` check 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 `isEvolutionArchetype` from `evolutionGenerator`, or validate against `CLAN_ORDER`/`[...CONTROLLED_ARCHETYPES, ...WILD_ARCHETYPES]`.
Author
Owner

Issue: failed finalist save says it will retry, but nothing schedules a retry

File/line: frontend/components/stream-control/control/BatchPanel.tsx:312

When the finalist save fails, the catch block resets persistedSeqRef.current = null and 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, batchLiveState and currentBatchCommand may not change again, and resetting a ref does not trigger a render. So after a transient failure in createMapper, createBatch, or attachBatchSlug, 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.

Issue: failed finalist save says it will retry, but nothing schedules a retry File/line: `frontend/components/stream-control/control/BatchPanel.tsx:312` When the finalist save fails, the catch block resets `persistedSeqRef.current = null` and 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, `batchLiveState` and `currentBatchCommand` may not change again, and resetting a ref does not trigger a render. So after a transient failure in `createMapper`, `createBatch`, or `attachBatchSlug`, 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.
beasty referenced this pull request from a commit 2026-06-12 14:03:34 +00:00
- BatchScene: derive the processed-cull count from the resumed stage
  instead of eliminatedIds.length, so reloading right after a cull
  landed (but before the stage advanced) still advances the stage
  rather than asking for a second cull on the same reveal.
- EvolutionPanel: validate restored clans against the full clan list
  (controlled + wild) — saved selections containing wild clans were
  rejected wholesale on restore/sync.
- BatchPanel: a failed finalist save now schedules an actual retry
  (5s timer re-triggering the persist effect) instead of only
  promising one; the terminal show state never re-runs the effect on
  its own.
- BatchCullBoard: drop dead stopPropagation calls and document that
  the corner controls are siblings of the card button, not nested
  inside it.

Powered by human calories and mass GPU cycles.
Author
Owner

All four review findings addressed in 125129b:

  1. BatchScene reload-after-cull race — fixed. seenEliminationsRef is now derived from the resumed stage (Math.min(stageIndex, maxEliminations)) instead of eliminatedIds.length. Each of the first maxEliminations stages 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.

  2. 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 misleading stopPropagation() calls (dead code — sibling clicks never reach the card handlers) and the comment implying nesting are removed.

  3. Wild clans rejected on restore — fixed. The follow effect now validates stored clans against CLAN_ORDER (controlled + wild) instead of CONTROLLED_ARCHETYPES, matching what the UI lets you select and what the sync effect writes.

  4. Phantom retry on failed finalist save — fixed. The catch block now schedules a real retry: a 5 s timer bumps a persistRetryTick state 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, and persistedSeqRef still prevents double saves.

Verified: typecheck, biome, and all 217 vitest tests pass.

All four review findings addressed in 125129b: 1. **BatchScene reload-after-cull race** — fixed. `seenEliminationsRef` is now derived from the resumed stage (`Math.min(stageIndex, maxEliminations)`) instead of `eliminatedIds.length`. Each of the first `maxEliminations` stages 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. 2. **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 misleading `stopPropagation()` calls (dead code — sibling clicks never reach the card handlers) and the comment implying nesting are removed. 3. **Wild clans rejected on restore** — fixed. The follow effect now validates stored clans against `CLAN_ORDER` (controlled + wild) instead of `CONTROLLED_ARCHETYPES`, matching what the UI lets you select and what the sync effect writes. 4. **Phantom retry on failed finalist save** — fixed. The catch block now schedules a real retry: a 5 s timer bumps a `persistRetryTick` state 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, and `persistedSeqRef` still prevents double saves. Verified: typecheck, biome, and all 217 vitest tests pass.
Author
Owner

Rechecked after 125129b8a8bf0a72eab10704165f56afeb8bbd80.

The four review findings are addressed:

  • Batch reload-after-cull: seenEliminationsRef is now based on the resumed stage rather than the current eliminated count.
  • Cull board DOM: the corner controls are siblings of the card button; I confirmed my nested-button finding was a false positive against the actual JSX structure, and the cleanup removes the misleading propagation code.
  • Evolution wild clans: restore validation now uses CLAN_ORDER, covering controlled and wild clans.
  • Finalist save retry: failed saves now schedule a real retry through persistRetryTick.

Verification passed locally: pnpm run typecheck, pnpm test -- --run, pnpm run lint (warning-only), and pnpm run build.

Rechecked after `125129b8a8bf0a72eab10704165f56afeb8bbd80`. The four review findings are addressed: - Batch reload-after-cull: `seenEliminationsRef` is now based on the resumed stage rather than the current eliminated count. - Cull board DOM: the corner controls are siblings of the card button; I confirmed my nested-button finding was a false positive against the actual JSX structure, and the cleanup removes the misleading propagation code. - Evolution wild clans: restore validation now uses `CLAN_ORDER`, covering controlled and wild clans. - Finalist save retry: failed saves now schedule a real retry through `persistRetryTick`. Verification passed locally: `pnpm run typecheck`, `pnpm test -- --run`, `pnpm run lint` (warning-only), and `pnpm run build`.
reportBatchStage now rejects regressions from a second overlay instance
(OBS source + control-page preview report independently): reports for
an earlier stage are ignored, the cull prompt can't be re-opened for a
stage whose cull already landed, and a report can't rewind awaitingCull
back to revealing — only a cull leaves the awaiting state.

attachViewSlug now also stamps the slug onto a wheel command derived
from the same spin (seq + 1, lastWheelSpinForSeq matching), so starting
the wheel before the history save finishes no longer loses the share QR.

Powered by human calories and mass GPU cycles.
Author
Owner

Both follow-up findings fixed in 3419c5c:

[P1] Stale batch stage reportsreportBatchStage now rejects regressions before patching:

  • a report for an earlier stageIndex than the stored one is ignored (monotonic stages per seq);
  • a same-stage report with awaitingCull: true is ignored once that stage's cull has already landed (eliminatedIds.length > stageIndex — each of the first maxEliminations stages takes exactly one cull), so the control board can never be re-prompted to cull from an old stage;
  • a same-stage report can't rewind awaitingCull → false: the only legitimate way out of the awaiting state is cullBatchCat itself.
    This also composes with the earlier resume fix: a reloaded overlay that briefly re-renders an already-culled stage now has its transient awaitingCull report suppressed server-side as well.

[P2] Spin QR lost on early wheelattachViewSlug now 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 reads currentCommand.viewSlug type-agnostically and slug patches don't bump seq, 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.

Both follow-up findings fixed in 3419c5c: **[P1] Stale batch stage reports** — `reportBatchStage` now rejects regressions before patching: - a report for an earlier `stageIndex` than the stored one is ignored (monotonic stages per seq); - a same-stage report with `awaitingCull: true` is ignored once that stage's cull has already landed (`eliminatedIds.length > stageIndex` — each of the first `maxEliminations` stages takes exactly one cull), so the control board can never be re-prompted to cull from an old stage; - a same-stage report can't rewind `awaitingCull → false`: the only legitimate way out of the awaiting state is `cullBatchCat` itself. This also composes with the earlier resume fix: a reloaded overlay that briefly re-renders an already-culled stage now has its transient `awaitingCull` report suppressed server-side as well. **[P2] Spin QR lost on early wheel** — `attachViewSlug` now 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 reads `currentCommand.viewSlug` type-agnostically and slug patches don't bump `seq`, 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.
beasty merged commit 11ab1bcf99 into main 2026-06-12 15:06:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
beasty/beastypage!7
No description provided.