fix(634): page the rerun-collection picker to completeness #645

Merged
timothy merged 3 commits from fix/634-rerun-picker-paging into main 2026-07-26 12:20:28 +02:00
Owner

fixes #634

What

SchedulesScreen loaded the rerun-collection picker with getRerunCollections({ pageNum: 0, pageSize: 1000 }). RerunCollectionController clamps pageSize to MaxPageSize = 100, so the request was always served 100 rows. Past 100 rerun collections the picker silently omitted the rest — no error, no truncation indicator, and a schedule item simply could not be pointed at a rerun collection that exists.

Replaced with loadAllRerunCollections(), which pages to completeness against totalCount at pageSize: 100, mirroring CollectionsScreen.enterReorder. The server cap is untouched: api.search-allitems-paging set the precedent that the client pages and the server stays bounded.

Done-when box 3 — the other loadPickerData fetches

Audited. getPlaylistGroups, getWatermarks, getGraphicsElements, getLanguages and getFillerPresetsByKind declare no paging parameters at all server-side and return plain arrays, so they are not truncatable and were left alone (a comment records why). This was confirmed twice independently — once against the controllers directly, and once against the full enumeration of paged /api/v1 operations built for #633.

But the class does not stop here. The cold review found, and grep confirmed, seven further call sites that ask for pageSize 500/1000 against endpoints capped at 100 — including RerunCollectionsScreen.tsx:136, which means this PR lets a schedule reference rerun collection #120 while the screen that creates and edits it still cannot show it. Deliberately not widened into this PR, whose test net is scoped to the schedules picker: filed as #644.

Verification

  • npx vitest run src/screens/SchedulesScreen.test.tsx → 27 passed; full web suite → 997 passed. tsc -b and eslint clean.
  • Mutation-verified: forcing the loop to stop after page 0 reddens the new test (expected 1 to be 2).
  • The new test pins the exact 150-element option set with toEqual and asserts exactly 2 requests — not a non-empty check, which cannot see items that are missing.

A latent trap this fixes on the way past

The existing #616 regression test looped over every rerun-collection request asserting pageNum === '0'. Correct when exactly one request was ever issued — but this branch makes the loader legitimately walk pages 1, 2, …, so that assertion now describes something the correct code does not do. It passed only because the shared fixture's totalCount fits in one page; raising that default would have failed the #616 test with a misleading "picker requested page 1" signal for what is proper paging. Narrowed to the first request, which is the offset #616 actually cared about.

The independent reviewer mutation-checked that narrowing rather than accepting it: reintroducing the exact #616 defect still fails the #616 test with the #616 message (and the #634 test too), and the rerunRequests[0] index fails cleanly via the preceding length assertion rather than throwing a TypeError.

Review

Independent cold review (process.independent-review-rubric: pure-SPA leaf, below every mandatory trigger, run anyway): MERGEABLE, twice — once on the implementation, then again on the fix commit at the current head. Two declined nits are recorded in the review thread: the concurrent-insert duplicate-row window (inherent to offset paging, identical in the mirrored precedent) and the seven sibling call sites (#644).

Docs

None required — no convention, route, endpoint or decision changed. The api.paging-zero-based contract this upholds is unchanged and is documented by #633.

fixes #634 ## What `SchedulesScreen` loaded the rerun-collection picker with `getRerunCollections({ pageNum: 0, pageSize: 1000 })`. `RerunCollectionController` clamps `pageSize` to `MaxPageSize = 100`, so the request was always served 100 rows. Past 100 rerun collections the picker silently omitted the rest — no error, no truncation indicator, and a schedule item simply could not be pointed at a rerun collection that exists. Replaced with `loadAllRerunCollections()`, which pages to completeness against `totalCount` at `pageSize: 100`, mirroring `CollectionsScreen.enterReorder`. The server cap is untouched: `api.search-allitems-paging` set the precedent that the client pages and the server stays bounded. ## Done-when box 3 — the other `loadPickerData` fetches Audited. `getPlaylistGroups`, `getWatermarks`, `getGraphicsElements`, `getLanguages` and `getFillerPresetsByKind` declare **no** paging parameters at all server-side and return plain arrays, so they are not truncatable and were left alone (a comment records why). This was confirmed twice independently — once against the controllers directly, and once against the full enumeration of paged `/api/v1` operations built for #633. **But the class does not stop here.** The cold review found, and grep confirmed, **seven** further call sites that ask for `pageSize` 500/1000 against endpoints capped at 100 — including `RerunCollectionsScreen.tsx:136`, which means this PR lets a schedule *reference* rerun collection #120 while the screen that creates and edits it still cannot show it. Deliberately **not** widened into this PR, whose test net is scoped to the schedules picker: filed as **#644**. ## Verification - `npx vitest run src/screens/SchedulesScreen.test.tsx` → 27 passed; full web suite → 997 passed. `tsc -b` and `eslint` clean. - **Mutation-verified**: forcing the loop to stop after page 0 reddens the new test (`expected 1 to be 2`). - The new test pins the **exact** 150-element option set with `toEqual` and asserts exactly 2 requests — not a non-empty check, which cannot see items that are missing. ## A latent trap this fixes on the way past The existing #616 regression test looped over **every** rerun-collection request asserting `pageNum === '0'`. Correct when exactly one request was ever issued — but this branch makes the loader legitimately walk pages 1, 2, …, so that assertion now describes something the correct code does not do. It passed only because the shared fixture's `totalCount` fits in one page; raising that default would have failed the #616 test with a misleading "picker requested page 1" signal for what is proper paging. Narrowed to the first request, which is the offset #616 actually cared about. The independent reviewer mutation-checked that narrowing rather than accepting it: reintroducing the exact #616 defect still fails the #616 test with the #616 message (and the #634 test too), and the `rerunRequests[0]` index fails cleanly via the preceding length assertion rather than throwing a `TypeError`. ## Review Independent cold review (`process.independent-review-rubric`: pure-SPA leaf, below every mandatory trigger, run anyway): MERGEABLE, twice — once on the implementation, then again on the fix commit at the current head. Two declined nits are recorded in the review thread: the concurrent-insert duplicate-row window (inherent to offset paging, identical in the mirrored precedent) and the seven sibling call sites (#644). ## Docs None required — no convention, route, endpoint or decision changed. The `api.paging-zero-based` contract this upholds is unchanged and is documented by #633.
timothy added 3 commits 2026-07-26 11:20:25 +02:00
SchedulesScreen loaded the rerun-collection picker with getRerunCollections({
pageNum: 0, pageSize: 1000 }). The server (RerunCollectionController) clamps
pageSize via Math.Clamp(pageSize, 1, MaxPageSize) with MaxPageSize=100, so
the request was silently served only the first 100 rows regardless of what
was asked for. With >100 rerun collections, the picker omitted the rest with
no error and no truncation indicator — a schedule item couldn't be pointed
at a rerun collection past the 100th.

Fix: page the client to completeness against totalCount, mirroring
CollectionsScreen.enterReorder (fetch page 0, keep requesting subsequent
pages while accumulated < totalCount, break early if a page returns zero
rows to guard against a non-terminating loop on a server-side anomaly).
Per api.search-allitems-paging precedent, the client pages rather than
raising the server's MaxPageSize cap.

Audited the other loadPickerData fetches (getPlaylistGroups, getWatermarks,
getGraphicsElements, getLanguages, getFillerPresetsByKind): their endpoints
return a plain, unpaged array server-side with no pageNum/pageSize params
and no clamp, so they aren't subject to the same silent-truncation defect
and don't need the same treatment.

Adds a vitest case pinning the exact expected option set (150 rerun
collections across two pages) rather than a non-empty/truthy check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The #616 comment this call site carried recorded WHY it reads `pageNum: 0` — a
previous version passed 1 and skipped the whole first page. Rewriting the call for
#634 dropped it. Restore it next to the new paging loop, which also starts its
follow-up requests at 1 and is only correct because the first page is 0.
fix(634): stop the #616 guard from asserting an invariant the fix violates
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 30s
review-verdict/h10 Review-verdict: MERGEABLE @ f9380eb
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m41s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 18m48s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 24m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
f9380eb494
Cold review finding. The #616 regression test looped over EVERY rerun-collection
request asserting pageNum === '0'. That was right when exactly one request was ever
issued, but since this branch the loader legitimately walks pageNum 1, 2, … to page to
completeness — so the assertion now describes something the correct code does not do.

It passes today only because the shared fixture's totalCount fits in a single page.
Raising that default would have failed the #616 test with a "picker requested page 1"
signal for what is proper paging, sending the next reader after a defect that isn't
there. Narrow it to the first request, which is the offset #616 actually cared about.

Also corrects the new comment's history: the `pageNum: 1` it describes is pre-#616,
not the previous commit.
Author
Owner

Review-verdict: MERGEABLE @ f9380eb

Independent cold review, twice: implementation (MERGEABLE) then the fix commit at this head (MERGEABLE, no findings at any severity). Reviewer mutation-verified the narrowed #616 guard itself. Two nits declined with reasons; the sibling truncation class filed as #644.

Review-verdict: MERGEABLE @ f9380eb Independent cold review, twice: implementation (MERGEABLE) then the fix commit at this head (MERGEABLE, no findings at any severity). Reviewer mutation-verified the narrowed #616 guard itself. Two nits declined with reasons; the sibling truncation class filed as #644.
timothy merged commit 5f3623b321 into main 2026-07-26 12:20:28 +02:00
timothy deleted branch fix/634-rerun-picker-paging 2026-07-26 12:20:33 +02:00
Sign in to join this conversation.