Files
ersatztv/docs/decisions/archive/spa/list-completeness-vs-bounded-pickers.md
T
timothyandClaude Opus 5 fad6805b91 feat(651): media-library pickers resolve by search instead of a bounded window
The three `getLibraryBrowseItems` pickers (RerunCollectionsScreen, PlaylistsScreen,
FillerPresetsScreen) populated a native <select> from a 100-row window over media-library
tables that can hold tens of thousands of rows. #644 made that truncation visible; it did
not make the picker usable, and paging to completeness would have been worse than the bug
(~200 serial requests, each more expensive than the last).

They now resolve by SEARCH through the shared `SearchPicker` over a new
`searchLibraryPickerOptions` helper: zero requests on mount or on a type switch, at most ONE
bounded request (25 rows) per settled query, nothing below 2 characters. Typed text is
compiled via the now-shared `titleContainsQuery` (`title:*<escaped>*`) rather than forwarded
raw, since the index's default field does not match bare title words. The current selection
renders from the owning record — `selectedName` for rerun collections and playlist items, and
for filler presets (which store only an id) a single by-id detail read — so editing an
existing record can never lose or fail to name its selection.

Class A stays put: bounded-by-construction admin lists still page to completeness via
`loadAllPages`, and the collection-family filler-preset types keep their bounded single page
(their `query` is a SQL LIKE, which a compiled Lucene query would not match). No server-side
cap is raised; this is a web-only change.

Folded in from #578: the rule-builder facet typeahead arms on focus rather than on mount (an
N-rule tree fired N unrequested lookups), both typeaheads pair their `seqRef` guard with a
shared `useIsMountedRef`, and the roundtrip test's LCG divides by 2^32 so `pick()` can no
longer index one past the end.

Decision record `spa.list-completeness-vs-bounded-pickers` is archived as superseded by the
new `spa.library-pickers-resolve-by-search`; spa-conventions §3b rewritten to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 04:25:14 +02:00

5.0 KiB

key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
key title status since supersedes superseded-by rule signals mechanics
spa.list-completeness-vs-bounded-pickers 2026-07-26 — `loadAllPages` is for bounded-by-construction lists only; media-library pickers stay bounded and show truncation (#644 follow-up) superseded 2026-07-26 none spa.library-pickers-resolve-by-search@2026-07-26 (superseded) The shared `loadAllPages` helper (`web/src/api/paging.ts`) pages a `/api/v1` list to completeness against `totalCount` and is used ONLY for lists that are bounded by construction (rerun collections, multi-collections, playlists — admin-created, hundreds of rows at most). A `getLibraryBrowseItems` picker over a media-library table (Episode/Song/Image/Movie/MusicVideo, tens of thousands of rows possible) must NOT page to completeness — it fetches ONE bounded page (the server cap) and surfaces the truncation (a `ctv-field-help` hint wired to the real `totalCount`) instead of silently dropping the rest. `loadAllPages`, Class A vs Class B picker, LuceneSearchIndex.Search hitsLimit, picker truncation hint, ctv-field-help, PagedResult, `complete` flag · paths: `web/src/api/paging.ts`, `web/src/screens/RerunCollectionsScreen.tsx`, `web/src/screens/PlaylistsScreen.tsx`, `web/src/screens/FillerPresetsScreen.tsx`, `web/src/screens/MultiCollectionsScreen.tsx`, `docs/spa-conventions.md` §3b · issues: #644 superseded by `spa.library-pickers-resolve-by-search` (ersatztv#651) — Class A (`loadAllPages` for bounded-by-construction lists) survives there unchanged; only the Class B rule is reversed. See `docs/spa-conventions.md` §3b

fe342a6a (#644) extracted the loadAllPages client-side paging helper and applied it at every call site that had been requesting an over-cap pageSize to "get everything in one call" — a pattern that silently truncated to the server's MaxPageSize (100) with no error and no truncation indicator. A cold adversarial review of that fix found it was correct for the admin-created lists (rerun collections, multi-collections, playlists — bounded by construction, hundreds of rows at most) but dangerous for three call sites: the getLibraryBrowseItems pickers in RerunCollectionsScreen, PlaylistsScreen, and FillerPresetsScreen, which populate a native <select> whose mediaType can be Episode, Song, Image, Movie, or MusicVideo — the largest tables in an install. Paging one of those to completeness means on the order of 200 serial requests against a 20,000-row library, each more expensive than the last (LuceneSearchIndex .Search computes hitsLimit = skip + limit, so later pages re-scan a growing prefix), ending in a <select> with 20,000 <option> nodes rendered into the DOM. That is worse than the defect #644 set out to fix.

The fix keeps loadAllPages unchanged in behavior for the bounded lists (it now also reports a complete: boolean flag and accepts an AbortSignal, per the same follow-up review's F4/F2 findings) and removes it entirely from the three media-library picker call sites. Those instead call getLibraryBrowseItems directly for a single page at the server cap (pageSize: 100) and read the response's totalCount to detect truncation. The defect named in #644's title is "silently truncate" — the silence is the bug, not the bound. So a truncated picker load renders a ctv-field-help hint next to the <select> (Showing the first 100 of 5000 — use search to narrow.) instead of either paging forever or truncating without saying so. A full typeahead/search-driven picker over the media library is a materially larger feature (a query param already exists on getLibraryBrowseItems for it) and is deliberately out of scope here — a follow-up issue, not this fix.

2026-07-26 addendum (round-3 review F1): loadPickerOptions's multi branch (a Class A source — MultiCollection) reused the same truncated: boolean field as the Class B media-library pickers, but the two conditions are not the same thing: Class B's truncated means "there are more rows than fit in one page — narrow via search," while a Class A picker's flag meant "the loadAllPages loop didn't converge" (complete: false) — a defensive/incomplete load, not a cap. Rendering both through the shared "Showing the first N of M — use search to narrow" copy produced a self-contradictory "Showing the first 47 of 47" on an incomplete Class A load, pointing at a search box that picker doesn't have. RerunCollectionsScreen.tsx/PlaylistsScreen.tsx now return a hint: 'incomplete' | 'none' | 'truncated' discriminator instead of a boolean, and render distinct copy per value — 'truncated' keeps the existing search-narrowing text, 'incomplete' renders "List may be incomplete — retry to reload" (matching the wording already used for the Class A list-load warn Badge). A picker's console.warn on an incomplete load — and the analogous one in SchedulesScreen.loadAllRerunCollections — is also gated on !signal?.aborted, so a superseded or user-aborted load (Retry, or a type switch mid-load) no longer logs a false "did not complete" warning.