Records the #128 review outcome (percent-fraction + poll-start-race fixes,bee686c8), maind63719ec, baselines (368/493+1skip/75 web), and new lessons (numeric wire-field units, trigger≠started grace windows, no ref mutation in updaters). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
ChicoryTV issue-queue handoff (living document)
Paste the prompt below into a fresh session to work the next item. Each session ends by UPDATING THIS FILE in place (rewrite the state section and the queue for the next item) so it always holds the current handoff. History: created 2026-07-02 after the plan audit (#59 epic) 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), #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 +
design-system agents; plus a fork verification pass over any fix diff). Codex implements.
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 (standing consent for this
stretch: merge when reviewers are happy and tests pass).
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/#128): fixtures must be what the actually-called endpoint
returns UNDER THE QUERY THE CLIENT SENDS. #128:
percentis 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<ConfigElementKey>()+<T>.Option<T>.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; NEW (#128):
LibraryScanStatusResponseModel.percentis 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 — #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), 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 (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 ciin 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.
- CONTRACT RULE: fixture values must be what the actually-called endpoint returns UNDER THE 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 #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 = 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:apiif stale):- 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
- Comment on issue #85 with findings + approach (prototype surface → endpoint/DTO mapping table, scope decisions, chosen window size) BEFORE coding.
- Implement; comment progress on #85 as you go.
- 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).
- 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 READ-ONLY (Fable runs its own review subagents). Include: PR number, branch, head SHA, base, files changed, the mapping table, verification commands + results, deferred/uncertain list, 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 #85, next prompt = #89 Channel Builder, record PR + main SHA + baselines), and pushes it to main.
Issue queue (work top-down)
- HOUSEKEEPING: #99 stays open for the final /api/channels/state onAir wiring; #126 (OpenAPI polymorphism gap) is a good backend slot-filler between screens.
- #85 Guide/EPG screen ← CODEX PROMPT above (Fable reviews via subagents, merges on consent, updates this file).
- 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.