fix: reviewer post-merge blockers — empty-lineup bare create (#212) + dirty-guard popstate/shuffle-toggle (#230) #242

Merged
timothy merged 5 commits from fix/212-230-review-blockers into main 2026-07-11 13:13:56 +02:00
Owner

Fixes the two blockers the adversarial reviewer returned CHANGES REQUIRED on in its post-merge verification of PR #229 (adversarial-reviewer#19, comment 7926): #212 reopened (bare-channel create unreachable on an empty lineup) and #230 promoted (dirty guard bypassed by popstate; shuffle-toggle leaves a stale item draft).

SPA + docs only — no dotnet changes.

Commits

  • d3d50f60 — #212 (reopened) + #230 finding 1. ChannelsEmptyState early-returned before the action bar that owns "New blank channel", so a fresh install could never create its first channel; it now offers both create paths, reusing the exact createBlankChannel handler (number = max+1 → 1 on an empty lineup, group "ErsatzTV", default ffmpeg profile). Same commit adds the App-level popstate dirty guard: browser Back/Forward can't be cancelled, so a vetoed pop re-pushes the pre-pop path via a currentPathRef read synchronously at event time (StrictMode-safe).
  • f09045e1 — spa-conventions §8 rewritten to document popstate coverage of the dirty guard.
  • 11b43d5e — #230 finding 2. A properties save that flips shuffleScheduleItems on the active schedule now reloads the items draft via GET so the server's EnforceProperties re-normalizes it; the Edit button is blocked while the item draft is dirty (confirm-to-discard, same semantics as guardedSwitch).

Behavior decision (Fix 3)

Chose block-edit-while-dirty over confirm-at-reload: a cancelled discard under confirm-at-reload would strand Fixed/Flood values in a now-shuffled schedule. Blocking the editor open while dirty guarantees the properties form only ever opens over a clean baseline draft, making the post-save reload lossless.

Pinning tests (per fix)

  • #212: App.test.tsx bare-creates from a [] lineup, asserts the POST payload (number: "1", group: "ErsatzTV", default ffmpeg profile) and nav to /app/edit-channel/{id}.
  • #230-1: App.test.tsx popstate cases — vetoed pop re-pushes the path; approved pop advances the route.
  • #230-2: SchedulesScreen.test.tsx — Fixed item on a non-shuffled schedule → edit properties to shuffle=true → a fresh items GET fires, the Fixed option is gone, and a subsequent Save's PUT carries no Fixed startType.

Web gate green locally: 582 tests / 65 files, lint, build.

🤖 Generated with Claude Code

Fixes the two blockers the adversarial reviewer returned CHANGES REQUIRED on in its post-merge verification of PR #229 (adversarial-reviewer#19, comment 7926): **#212 reopened** (bare-channel create unreachable on an empty lineup) and **#230 promoted** (dirty guard bypassed by popstate; shuffle-toggle leaves a stale item draft). SPA + docs only — no dotnet changes. ## Commits - **d3d50f60** — #212 (reopened) + #230 finding 1. `ChannelsEmptyState` early-returned before the action bar that owns "New blank channel", so a fresh install could never create its first channel; it now offers both create paths, reusing the exact `createBlankChannel` handler (number = max+1 → **1** on an empty lineup, group `"ErsatzTV"`, default ffmpeg profile). Same commit adds the App-level `popstate` dirty guard: browser Back/Forward can't be cancelled, so a vetoed pop re-pushes the pre-pop path via a `currentPathRef` read synchronously at event time (StrictMode-safe). - **f09045e1** — `spa-conventions` §8 rewritten to document popstate coverage of the dirty guard. - **11b43d5e** — #230 finding 2. A properties save that flips `shuffleScheduleItems` on the active schedule now reloads the items draft via GET so the server's `EnforceProperties` re-normalizes it; the Edit button is blocked while the item draft is dirty (confirm-to-discard, same semantics as `guardedSwitch`). ## Behavior decision (Fix 3) Chose **block-edit-while-dirty** over confirm-at-reload: a cancelled discard under confirm-at-reload would strand Fixed/Flood values in a now-shuffled schedule. Blocking the editor open while dirty guarantees the properties form only ever opens over a clean baseline draft, making the post-save reload lossless. ## Pinning tests (per fix) - **#212**: `App.test.tsx` bare-creates from a `[]` lineup, asserts the POST payload (`number: "1"`, `group: "ErsatzTV"`, default ffmpeg profile) and nav to `/app/edit-channel/{id}`. - **#230-1**: `App.test.tsx` popstate cases — vetoed pop re-pushes the path; approved pop advances the route. - **#230-2**: `SchedulesScreen.test.tsx` — Fixed item on a non-shuffled schedule → edit properties to shuffle=true → a fresh items GET fires, the Fixed option is gone, and a subsequent Save's PUT carries no Fixed startType. Web gate green locally: 582 tests / 65 files, lint, build. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 3 commits 2026-07-11 12:45:21 +02:00
ChannelsScreen returned ChannelsEmptyState before the action bar that owns
"New blank channel", so a fresh install could never create its first channel.
The empty state now offers both create paths (bare-create + ChannelBuilder),
reusing the exact createBlankChannel handler (number = max+1 → 1 on empty,
group "ErsatzTV", default ffmpeg profile).

Regression: App.test.tsx bare-creates from a [] lineup, asserts the POST
payload (number "1") + navigation to /app/edit-channel/{id}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
canLeaveCurrentScreen() was only consulted in App's navigate() (sidebar/nav
clicks); browser Back/Forward switched screens unguarded. A popstate can't be
cancelled, so App's popstate handler now, on a vetoed guard, re-pushes the
pre-pop path (tracked in currentPathRef, updated on every approved navigation)
and leaves activeRoute untouched — undoing the browser's URL change. The same
handler covers the synthetic pop navigateToPath() dispatches. Re-pushing is
safe: only one screen is mounted at a time and the guard-registering screen
(schedules) owns no internal popstate listener, so no sub-path screen's
pathname state can desync. Effect cleanup keeps StrictMode double-mount from
double-registering.

Docs: spa-conventions §8 rewritten from navigate-only to describe popstate
coverage. Regression: App.test.tsx dirties the schedules draft, simulates
popstate → confirm called; cancel keeps route + re-pushes path; accept
switches route and unmounts the draft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(spa): reload item draft when a save flips shuffleScheduleItems (#230 finding 2)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
11b43d5e06
Editing a schedule's properties to change shuffleScheduleItems for the active
schedule left the open items draft stale: hidden Fixed/Flood start values could
be saved back and the inspector kept offering start-type controls the schedule
no longer supports. onScheduleSaved now detects a shuffle flip on the active
schedule and reloads the items via GET so the server's EnforceProperties
re-normalizes the draft. The schedule-level flags already refresh from the save
response (setBoot maps `saved` into the list).

Decision: chose "block opening the edit dialog while the item draft is dirty"
(confirm-to-discard, guardedSwitch semantics) over confirm-at-reload — the
smaller fully-consistent change. It guarantees the properties editor only ever
opens over a clean baseline draft, so the post-save reload is lossless and
avoids the awkward state where a cancelled discard leaves a now-shuffled
schedule holding Fixed values.

Regression: SchedulesScreen.test.tsx — Fixed item on a non-shuffled schedule →
edit properties to shuffle=true → a fresh items GET fires, the Fixed option is
gone, and a subsequent Save's PUT carries no Fixed startType.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy added 1 commit 2026-07-11 12:55:13 +02:00
fix(spa): review follow-ups — surface empty-state create error, gate Edit while saving
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
6f6fcf6c63
Two findings from the PR #242 adversarial review (Codex + Claude fork):

1. ChannelsEmptyState swallowed mutationError: on the fresh-install path #212
   targets, a bare-create can 4xx (e.g. no default ffmpeg profile), but the empty
   branch never rendered the error alert the non-empty screen shows — the user saw
   only a spinner re-enable. The empty state now renders the same ctv-channels-error
   alert. Regression: App.test.tsx asserts the error surfaces + no navigation on an
   empty lineup.

2. SchedulesScreen Edit button was not gated on `saving` (Codex): during an items
   save PUT the draft is still dirty, so a discard-to-open → shuffle-flip could let
   the in-flight PUT resolve AFTER the shuffle reload and clobber the normalized
   draft with the pre-shuffle body. Edit is now disabled while saving (consistent
   with the schedule Select). Regression: a deferred PUT proves Edit is disabled
   in-flight and re-enables once the save settles.

Deferred as nits (both reviews rate low): vetoed navigateToPath leaves two stray
history entries (cosmetic, rare — fixing means refactoring central nav); rapid
double-Back is best-effort (inherent popstate non-cancellability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Adversarial review pass (Codex + scoped Claude fork) — findings applied

Both reviewers examined the diff independently. Applied as follow-up commit 6f6fcf6c:

  1. ChannelsEmptyState swallowed mutationError (both reviewers, should-fix) — on the exact fresh-install path #212 targets, a bare-create can 4xx (e.g. no default ffmpeg profile) and the empty branch never rendered the error the non-empty screen shows. Now renders the same ctv-channels-error alert; regression test asserts the error surfaces with no navigation on an empty lineup.
  2. Edit not gated on saving (Codex, flagged blocker) — during an items-save PUT the draft is still dirty, so a discard-to-open → shuffle-flip could let the in-flight PUT resolve after the shuffle reload and clobber the normalized draft. Edit is now disabled={saving} (consistent with the schedule Select); regression test with a deferred PUT proves Edit is disabled in-flight and re-enables once the save settles.

Deferred as nits (both reviews rate low): a vetoed navigateToPath leaves two stray history entries (cosmetic; only reachable from a dirty schedules screen, and fixing it means refactoring the central nav function — out of scope for a targeted blocker fix); rapid double-Back is best-effort (inherent popstate non-cancellability, acknowledged in the code comment).

Web gate green: 584 tests / 65 files, lint, build.

### Adversarial review pass (Codex + scoped Claude fork) — findings applied Both reviewers examined the diff independently. **Applied as follow-up commit `6f6fcf6c`:** 1. **`ChannelsEmptyState` swallowed `mutationError`** (both reviewers, should-fix) — on the exact fresh-install path #212 targets, a bare-create can 4xx (e.g. no default ffmpeg profile) and the empty branch never rendered the error the non-empty screen shows. Now renders the same `ctv-channels-error` alert; regression test asserts the error surfaces with no navigation on an empty lineup. 2. **Edit not gated on `saving`** (Codex, flagged blocker) — during an items-save PUT the draft is still dirty, so a discard-to-open → shuffle-flip could let the in-flight PUT resolve *after* the shuffle reload and clobber the normalized draft. Edit is now `disabled={saving}` (consistent with the schedule Select); regression test with a deferred PUT proves Edit is disabled in-flight and re-enables once the save settles. **Deferred as nits** (both reviews rate low): a vetoed `navigateToPath` leaves two stray history entries (cosmetic; only reachable from a dirty schedules screen, and fixing it means refactoring the central nav function — out of scope for a targeted blocker fix); rapid double-Back is best-effort (inherent popstate non-cancellability, acknowledged in the code comment). Web gate green: 584 tests / 65 files, lint, build.
timothy added 1 commit 2026-07-11 13:06:07 +02:00
fix(spa): gate Delete on saving to close the last switch-during-save clobber
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
b26045676b
Codex re-review of the prior fix commit found the Edit gate closed the exact
repro but two paths remained. One is reachable: Delete was the only
schedule-switch path that guardedSwitch's `saving` guard didn't cover — deleting
mid-save runs applySwitch to the next schedule while the in-flight items PUT is
still outstanding, and that PUT's completion handler then overwrites the next
schedule's draft with the deleted schedule's response. Delete is now
`disabled={saving}`, consistent with the Select, Edit, and guardedSwitch.

Regression: the deferred-PUT test now also asserts Delete is disabled in-flight
and re-enables after the save settles.

Also widens the test mock's onRequest return type to `Response | Promise<Response>
| null` (removes the `as unknown as Response` cast — a test-only type hole the
re-review flagged).

Deferred to #248: the other residual path (properties dialog not focus-trapped, so
keyboard focus can escape to underlying Add/Save mid-save) is a pre-existing,
cross-cutting overlay.tsx a11y gap affecting all dialogs — out of scope for this
targeted blocker fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy merged commit eba22ce679 into main 2026-07-11 13:13:56 +02:00
timothy deleted branch fix/212-230-review-blockers 2026-07-11 13:13:56 +02:00
Sign in to join this conversation.