diff --git a/docs/handoffs/chicorytv-issue-queue.md b/docs/handoffs/chicorytv-issue-queue.md index 707d65d8b..c966e873e 100644 --- a/docs/handoffs/chicorytv-issue-queue.md +++ b/docs/handoffs/chicorytv-issue-queue.md @@ -6,7 +6,8 @@ always holds the current handoff. History: created 2026-07-02 after the plan aud filed backend gap issues #100–#111; a 7–9-way parallel workflow build once exhausted RAM, so builds are limited to 2–3 concurrent, never wide fan-outs. Backend gaps all landed by 2026-07-04 (PRs #113–#119); merge pass PR #120; live-data screens so far: #109 Dashboard -(PR #123), #84 Channels (PR #124), #86 Schedule editor (PR #125), #87 Playouts (PR #127). +(PR #123), #84 Channels (PR #124), #86 Schedule editor (PR #125), #87 Playouts (PR #127), +#88 Libraries (PR #128). **PROCESS (2026-07-05, binding)**: Fable writes each Codex prompt AND performs the PR review (via review subagents — parallel lenses: correctness fork + cheaper contract/tests + @@ -14,129 +15,132 @@ design-system agents; plus a fork verification pass over any fix diff). Codex im Every Codex prompt MUST mandate: (a) subagents where appropriate at fitting effort/model levels, (b) `npm ci` in each fresh worktree. Review-driven fixes are applied by fitting subagents too (NITS: Fable fixes on the branch; SUBSTANTIAL: back to Codex verbatim or fixed -in-session — user decides). Merges need explicit user consent. +in-session — user decides). Merges need explicit user consent (standing consent for this +stretch: merge when reviewers are happy and tests pass). -**Session state (2026-07-05, post-#87)**: main = a8dd88ef (PR #127 merge): Playouts monitor -live — selector rail (paged /api/playouts + filter), now-playing hero from the 30s -channel-state poll, up-next/timeline/upcoming from /api/playouts/{id}/items (fetch on -selection + Show filler toggle → ?showFiller=true; never polled), warnings badge, reset-all -(confirm + ref guard). Fable review found 2 blocker-class defects fixed pre-merge -(c4190e3a, sonnet+haiku subagents, fork-verified CONFIRMED-GOOD): the filler UI was wrong -AND dead (see lesson below) and an undefined CSS token broke the hero. Baselines: -ErsatzTV.Tests **368**, Core.Tests **493** (+1 skip); web tests **63**, lint/typecheck/build -clean. #84/#86/#87 CLOSED. Worktree .worktrees/issue-87-playouts is merged — remove it. -Main checkout sits on docs/59-ui-redesign-brief (fully merged, safe to switch). +**Session state (2026-07-05, post-#88)**: main = d63719ec (PR #128 merge): Libraries screen +live — source cards (media-sources), per-library scan progress + triggers, per-source scan, +conditional 10s scan-status polling (pending∪active drives the interval; 3-tick grace window +covers the queue-to-start lag; drains on persistent errors; refetches sources once on +completion). Review found 2 production-visible substantials fixed pre-merge (bee686c8): +percent was a 0–1 wire fraction (bar showed ~0%), and the poll-start race. Fork verification +caught a StrictMode-unsafe ref mutation inside a setState updater — fixed via a pure hoist. +Baselines: ErsatzTV.Tests **368**, Core.Tests **493** (+1 skip); web tests **75**, +lint/typecheck/build clean. #84/#86/#87/#88 CLOSED. Main checkout sits on +docs/59-ui-redesign-brief (fully merged, safe to switch). **Lessons for all remaining prompts** (accumulated): -- Fixture fidelity (#109, #127): fixture values MUST be what the actually-called endpoint - returns under the query the client actually sends. #127's filler bug: backend always emits - fillerKind ('None' for real items — truthy!), while the client never sent showFiller=true - (server strips filler by default) — fixtures invented null/'PreRoll' and masked both. When - a field is enum-with-None, never truthiness-check it; when an endpoint has query params - that change the response, tests must cover the param actually sent. -- OpenAPI can UNDER-report the wire (abstract VMs + Newtonsoft runtime-type serialization, - #125/#126) — check the backend serializer before hand-widening types; file contract gaps. -- setState updaters must be PURE — no fetches inside them (StrictMode double-invokes; #127 - used a baseRef hoist). Guard double-submit with a REF (#125). Merge poll results - functionally — never write captured snapshots over newer state (#127). -- Every new multi-column grid goes into the @media (max-width: 980px) collapse block (#127 - missed all of them → page-level horizontal scroll). var() fallbacks must equal the token's - resolved value; verify the token EXISTS (--ctv-surface-1 didn't; --text-faint doesn't — - use --text-disabled). No page-level horizontal scroll ever. -- Prototype affordances are either implemented or VISIBLY deferred (disabled + note) — never - silently dropped (#127 dropped Reset buttons + the Show filler toggle; the toggle turned - out to be the missing half of a blocker). -- Anything actionable derives from what is VISIBLE (#84); disable all mutation triggers while - mutating (incl. rails/rows); mutations never refetch the world (#125/#127). -- A screen listing N entities must let the user pick among them (#125). -- Tests must be honest: no scenarios the backend cannot produce (reset-all has NO error path - — unconditional 202); mount call-count tests must isolate the route's own fetches from - Dashboard prefetch (assert the DELTA across navigation); paging labels must not lie when - totalCount > page length ("shown of total"); test search/filter UI you ship. -- The image-build job runs ONLY on main pushes — PR CI can't catch Dockerfile breakage. -- DTO records in ErsatzTV.Core/Api need file-scoped `#nullable enable`; ErsatzTV.Application - has NO nullable context. NSubstitute+ConfigElementKey: `Arg.Any()` + ``. - `Option.ToNullable()` doesn't exist → `MatchUnsafe(v => (T?)v, () => null)`. +- Fixture fidelity (#109/#127/#128): fixtures must be what the actually-called endpoint + returns UNDER THE QUERY THE CLIENT SENDS. #128: `percent` is a 0–1 fraction despite its + name (bar showed ~0%; 0–100 fixtures masked it). Enum-with-None fields are never + truthiness-checked. Verify UNITS/scale of numeric wire fields against the producing code, + not the field name. +- Trigger≠started (#128): a 202/200 on a trigger endpoint means QUEUED; status endpoints may + lag by seconds. Pending-state with a grace window bridges the gap; poll while + pending∪active nonempty; drain grace on persistent errors so UIs can't stick. +- setState updaters must be PURE — no fetches (#127) and no ref mutations (#128); hoist + side-effects/bookkeeping out. StrictMode double-invokes updaters and the test renderer + doesn't, so tests won't catch it — reviewers must. +- OpenAPI can UNDER-report the wire (#125/#126); check the serializer before widening types. +- Every new multi-column grid → the @media (max-width: 980px) collapse block; var() fallback + = the token's resolved value; verify the token EXISTS (--ctv-surface-1, --text-faint don't). +- Prototype affordances: implemented or VISIBLY deferred — never silently dropped + (#127 toggle = half a blocker; #128 per-source Scan). +- Actionable = visible (#84); disable all mutation triggers while mutating; ref-based + double-submit guards; mutations never refetch the world (#125/#127). +- A screen listing N entities lets the user pick among them (#125). +- Honest tests: no scenarios the backend can't produce (no exception middleware → failures + are BARE 4xx/5xx, not ProblemDetails, unless the controller explicitly returns them); + mount call-counts assert the DELTA across navigation (Dashboard prefetches + /api/playouts AND /api/media-sources); paging labels don't lie; test the search/filter UI + you ship; status-dot state never color-only (StatusDot has a label prop). +- The image-build job runs ONLY on main pushes. DTO records in Core/Api need `#nullable + enable`; Application has NO nullable context. NSubstitute+ConfigElementKey: + `Arg.Any()` + ``. `Option.ToNullable()` → `MatchUnsafe`. update-openapi.sh needs a prior normal build. Child GETs 404 unknown parents via pre-check. - Backlog nits (unfiled): unclamped pageSize; PlayoutController Create/Delete lack Name=; heavy GetItems pre-check; >30 MB uploads → bare 413; artwork content-type trusted (#66); - schedule estimator materializes collections per GET; /api/health TTL cache; #85 EPG: - /api/guide 21-include eager-load + fillerKind-mostly-None notes; no Dialog/Modal component - yet (likely needed by #89); 1 pre-existing --text-faint usage in shell.css. Filed: #126 - (OpenAPI polymorphism gap — also unblocks removing the widened type in schedules.ts). + schedule estimator materializes collections per GET; /api/health TTL cache; NEW (#128): + `LibraryScanStatusResponseModel.percent` is a 0–1 fraction under a percent name — rename + or ×100 server-side eventually; no Dialog/Modal component yet (likely needed by #89); + 1 pre-existing --text-faint usage in shell.css. Filed: #126 (OpenAPI polymorphism gap). --- -# PROMPT FOR CODEX — #88: Libraries screen (media source cards + scan status) +# PROMPT FOR CODEX — #85: Guide / EPG screen (channels × time grid) You are Codex, the IMPLEMENTER, in /Users/timothy/ersatztv (ErsatzTV fork; React SPA "ChicoryTV" in web/ — Vite + TS, typed client web/src/api/, generated types web/src/api/generated/v1.d.ts). Fable (Claude) reviews your PR read-only afterwards — do NOT merge. Read CLAUDE.md and docs/contributing.md first; follow the SPA patterns from Channels -(PR #124), Schedule editor (PR #125), and Playouts (PR #127) — discriminated-union query -hooks, messageFromError/ProblemDetails, mutation discipline — match, don't invent. +(PR #124), Schedule editor (PR #125), Playouts (PR #127), Libraries (PR #128) — match, don't +invent. HARD CONSTRAINTS: - USE SUBAGENTS where appropriate, at fitting effort and model levels: cheap/fast agents for mechanical work (fixtures, CSS transcription, test boilerplate); higher-effort agents only - for judgment work (scan-status polling state machine, API-contract mapping). Keep - orchestration and final assembly in your main session. -- Worktree: `git worktree add .worktrees/issue-88-libraries -b feat/88-libraries origin/main`. + for judgment work (grid virtualization/layout math, time-window state). Keep orchestration + and final assembly in your main session. +- Worktree: `git worktree add .worktrees/issue-85-epg -b feat/85-epg origin/main`. Never touch the main checkout or other .worktrees/*. - Run `npm ci` in web/ in the worktree BEFORE any lint/typecheck/test/build. - Max 2–3 concurrent builds machine-wide; ONE dotnet build at a time here. - NEVER set ETV_UPDATE_GOLDENS. A golden-file diff means your code is wrong. - FRONTEND-only: no new endpoints, no ErsatzTV.Core/Application changes. Missing data = - VISIBLE deferral (disabled + note) — never silently drop a prototype affordance (#127 - lesson: a silently-dropped toggle was half of a blocker). + VISIBLE deferral (disabled + note) — never silently drop a prototype affordance. - CONTRACT RULE: fixture values must be what the actually-called endpoint returns UNDER THE - QUERY THE CLIENT SENDS — copy from backend mappers/serializers or v1.d.ts, never invent. - Enum-with-None fields must never be truthiness-checked (#127's filler bug). If generated - types look incomplete vs the backend, check the serializer first (see #126). + QUERY THE CLIENT SENDS; verify units/scale of numeric fields against the producing backend + code, never the field name (#128's percent lesson). Enum-with-None fields are never + truthiness-checked (#127's fillerKind lesson — this endpoint HAS fillerKind!). ## Task -Issue #88: recreate design-system/templates/chicorytv-admin/ScheduleLibrary.jsx (Libraries) -wired to the live API. Local/Plex/Jellyfin/Emby source cards with per-library scan status + -scan triggers. Acceptance: matches prototype; live sources + scan triggers; all 3 themes. +Issue #85: recreate design-system/templates/chicorytv-admin/Epg.jsx wired to the JSON guide. +Dark channels×time grid, "now" marker, live focal color, jump-to-now. Acceptance: matches +prototype; live guide data; all 3 themes. -## Context (main = a8dd88ef; baselines: ErsatzTV.Tests 368, Core.Tests 493+1skip, web 63) +## Context (main = d63719ec; baselines: ErsatzTV.Tests 368, Core.Tests 493+1skip, web 75) - Gitea: http://192.168.1.95:3000/timothy/ersatztv (basic auth timothy:ded89Lm4). - API surface (verify in v1.d.ts; regen `npm run generate:api` if stale): - - GET /api/media-sources → MediaSourceResponseModel[] { id, kind (STRING — copy the exact - values from the backend mapper, do not guess casing), name, connectionAddress (nullable), - libraries: [{ id, name, mediaKind, lastScan (nullable ISO), itemCount }] }. - - GET /api/libraries/scan-status → LibraryScanStatusResponseModel[] { libraryId, percent } - — the in-progress scan progress source. - - POST /api/libraries/{id}/scan — trigger a scan (check the controller for status code + - body + error responses; verify whether a ProblemDetails error path exists — write tests - only for scenarios the backend CAN produce, #127 lesson). - - POST /api/libraries/{id}/scan-show exists — include only if the prototype surfaces it, - else ignore (not even a deferral note needed). - - All errors ProblemDetails — surface `detail`. -- Polling policy for scan status: fetch sources + scan-status on mount; poll - /api/libraries/scan-status ONLY while at least one scan is in progress (i.e. after the - user triggers a scan or mount shows nonzero in-progress entries), at ≥10s, and STOP the - interval when no scans remain in progress; refetch /api/media-sources once when a scan - completes (to refresh lastScan/itemCount). Never poll /api/media-sources itself. NO - /api/health polling. -- Scan trigger mutation: disabled-while-scanning for that library (and while the trigger - request is in flight — ref guard), ProblemDetails banner on failure, per-library progress - bar from percent. -- CSS: design-system tokens only; verify each token EXISTS; var() fallback = resolved value; - add every new multi-column grid to the @media (max-width: 980px) collapse block. -- Tests: mount fetch delta (isolate from Dashboard prefetch); poll starts only when a scan - is in progress and STOPS when done; scan trigger success + (only-if-producible) failure; - empty sources; error+retry; fixture `kind` values copied from the backend mapper. + - GET /api/guide?start=&end= (both optional DateTimeOffset; start defaults to + now, end to start + configured XmltvDaysToBuild) → ChannelGuideResponseModel { start, + end, channels: [{ number, name, programmes: [{ start, stop, title, subTitle (nullable), + category (nullable), fillerKind }] }] }. + - CRITICAL COST NOTE (#102): the guide endpoint runs a heavy 21-include eager-load PER + CHANNEL PER REQUEST. Request a BOUNDED window (e.g. now-1h → now+12h), fetch ONCE on + mount + on explicit user navigation (prev/next window, jump-to-now beyond the loaded + range) — NEVER poll /api/guide. The "now" marker advances client-side on a timer + (minute-ish tick moving the marker/highlight; NO refetch). + - fillerKind in the JSON guide is almost always 'None' — the shared projector merges + filler into adjacent programmes. Do NOT truthiness-check it; discrete filler rows would + need a backend projection mode (out of scope — no UI toggle for it). + - GET /api/channels/state (onAir) may drive the live focal treatment; if used, poll ≥30s + (Channels precedent) — it's the only thing allowed on an interval besides the client-side + now-marker tick. + - Channel logos: check what the prototype uses; the SPA has ChannelLogo (initials-based). + /iptv logo URLs exist but check what Channels/Dashboard screens do — match precedent. +- Grid math: programmes can start before / end after the requested window — clip to the + window; overlapping/adjacent programmes must not produce negative/NaN widths; empty + channels (no programmes) render an empty row, not a crash; timezone = render in the + BROWSER's local time (the wire carries offsets; Date parsing is TZ-safe). +- No mutations on this screen. Read-only + navigation state only. +- CSS: design-system tokens only; verify each token EXISTS; fallback = resolved value; new + multi-column grids → the 980px collapse block; the grid scrolls in its OWN container + (both axes as needed) — never page-level horizontal scroll. +- Tests: mount fetch delta (guide fetched exactly once; no guide polling — assert interval + ticks don't refetch /api/guide); window navigation triggers exactly one fetch per step; + jump-to-now; now-marker position math (fixed fake now vs fixture times); programme + clipping at window edges; empty channels; error+retry; fixture fillerKind 'None' (real + wire value) with subTitle/category nulls exercised. ## Process -1. Comment on issue #88 with findings + approach (prototype surface → endpoint/DTO mapping - table, scope decisions) BEFORE coding. -2. Implement; comment progress on #88 as you go. +1. Comment on issue #85 with findings + approach (prototype surface → endpoint/DTO mapping + table, scope decisions, chosen window size) BEFORE coding. +2. Implement; comment progress on #85 as you go. 3. Verify: cd web && npm ci && npm run lint && npm run typecheck && npm test -- --run && npm run build; then TZ=UTC dotnet build ErsatzTV.sln; TZ=UTC dotnet test ErsatzTV.Tests + Core.Tests sequentially (expect 368 / 493+1skip — no backend regression). -4. Push, open PR → main: "feat(web): Libraries screen (#88)", body lists the mapping table - and scope decisions; `closes #88`. Poll CI by head SHA until green. Do NOT merge. +4. Push, open PR → main: "feat(web): Guide / EPG screen (#85)", body lists the mapping table + and scope decisions; `closes #85`. Poll CI by head SHA until green. Do NOT merge. ## On completion — REQUIRED final output Print a fenced handoff prompt addressed to Claude (Fable) asking it to review the PR @@ -145,21 +149,18 @@ files changed, the mapping table, verification commands + results, deferred/unce and the finding classification (NITS = Fable fixes on the branch; SUBSTANTIAL = back to Codex verbatim or fixed in-session with subagents — user decides). After approval + user merge consent, Fable merges, verifies main's post-merge run (image job included), updates -THIS handoff (pop #88, next prompt = #85 EPG, record PR + main SHA + baselines), and pushes -it to main. +THIS handoff (pop #85, next prompt = #89 Channel Builder, record PR + main SHA + baselines), +and pushes it to main. --- ## Issue queue (work top-down) -0. HOUSEKEEPING: remove .worktrees/issue-87-playouts (merged). #99 stays open for the final - /api/channels/state onAir wiring; #126 (OpenAPI polymorphism gap) is a good backend - slot-filler between screens. -1. #88 Libraries screen ← CODEX PROMPT above (Fable reviews via subagents, merges on +0. HOUSEKEEPING: #99 stays open for the final /api/channels/state onAir wiring; #126 + (OpenAPI polymorphism gap) is a good backend slot-filler between screens. +1. #85 Guide/EPG screen ← CODEX PROMPT above (Fable reviews via subagents, merges on consent, updates this file). -2. UX conversion order: #85 EPG (#102 JSON guide; mind fillerKind + per-request-cost notes - above) → #89 Channel Builder (#104 artwork upload; also needs #62: #63/#64/#65; likely - needs the first Dialog/Modal component; languages endpoint follow-up from #105 when - needed) → #93 Settings → #90 rebrand → #91 cutover. -Cross-refs: #99 seam landed (PR #121), final wiring open. Done recently: PR #124 (#84 -Channels), PR #125 (#86 Schedule editor), PR #127 (#87 Playouts — closed 2026-07-05, main -a8dd88ef). Filed: #126. +2. UX conversion order: #89 Channel Builder (#104 artwork upload; also needs #62: + #63/#64/#65; likely needs the first Dialog/Modal component; languages endpoint follow-up + from #105 when needed) → #93 Settings → #90 rebrand → #91 cutover. +Cross-refs: #99 seam landed (PR #121), final wiring open. Done recently: PR #125 (#86), +PR #127 (#87), PR #128 (#88 Libraries — closed 2026-07-05, main d63719ec). Filed: #126.