docs: advance ChicoryTV issue queue past #85 (PR #129); next prompt = #65 library browse API (Codex)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m39s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m14s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m32s
Renovate / Renovate (push) Successful in 1m18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m39s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m14s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m32s
Renovate / Renovate (push) Successful in 1m18s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,9 @@ UPDATING THIS FILE in place (rewrite the state section and the queue for the nex
|
||||
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).
|
||||
2026-07-04 (PRs #113–#119); merge pass PR #120; live-data screens: #109 Dashboard (PR #123),
|
||||
#84 Channels (PR #124), #86 Schedule editor (PR #125), #87 Playouts (PR #127), #88 Libraries
|
||||
(PR #128), #85 Guide/EPG (PR #129).
|
||||
|
||||
**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 +
|
||||
@@ -18,149 +18,155 @@ subagents too (NITS: Fable fixes on the branch; SUBSTANTIAL: back to Codex verba
|
||||
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).
|
||||
**Session state (2026-07-05, post-#85)**: main = 8a62f222 (PR #129 merge): Guide/EPG screen
|
||||
live — bounded 13h JSON guide window (never polled; the endpoint is expensive per #102),
|
||||
client-side now marker on a 60s tick, /api/channels/state at 30s for live dots, prev/next/
|
||||
jump-to-now each = exactly one guide fetch, programme clipping at window edges. Fable's
|
||||
8-finder/per-candidate-verifier review found 1 CONFIRMED blocker (crash: `nowPlaying === null`
|
||||
guard vs field OMITTED on the wire → `undefined` → TypeError on any on-air channel playing
|
||||
offline-fallback filler) + 1 CONFIRMED functional bug (live highlight matched on title, but
|
||||
guide titles are show-only vs nowPlaying's `GetDisplayTitle` — could never match episodes);
|
||||
Codex fixed both in a8d4979 (truthiness guard + wire-shape fixture; timestamps-only match)
|
||||
plus 3 cleanups. Deferred cleanups → backlog nits below. #85 CLOSED. Baselines:
|
||||
ErsatzTV.Tests **368**, Core.Tests **493** (+1 skip); web tests **80**; lint/typecheck/build
|
||||
clean. Main checkout sits on docs/59-ui-redesign-brief (fully merged, safe to switch).
|
||||
|
||||
**Lessons for all remaining prompts** (accumulated):
|
||||
- NULL FIELDS ARE OMITTED ON THE WIRE (#129): Startup.cs sets Newtonsoft
|
||||
`NullValueHandling.Ignore` globally, so any null DTO property is ABSENT from the JSON →
|
||||
`undefined` in the browser, even though generated types say `| null`. Frontend guards must
|
||||
use truthiness (`!x`), NEVER `=== null`; fixtures for null cases must OMIT the key (test
|
||||
precedent: "renders the Guide screen when an on-air channel omits nowPlaying").
|
||||
- Cross-endpoint correlation needs shared ids (#129): /api/guide titles are show-only
|
||||
(ChannelGuideMetadata.GetTitle) while /api/channels/state nowPlaying uses GetDisplayTitle
|
||||
("Show - s01e01 - Ep") — string matching across endpoints can never work for episodes.
|
||||
Live match is now timestamps-only; the real fix is a shared programme/playout-item id on
|
||||
both endpoints (backlog).
|
||||
- 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.
|
||||
returns UNDER THE QUERY THE CLIENT SENDS. `percent` is a 0–1 fraction despite its name.
|
||||
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; poll while
|
||||
pending∪active nonempty with a grace window; drain grace on persistent errors.
|
||||
- setState updaters must be PURE — no fetches (#127), no ref mutations (#128); StrictMode
|
||||
double-invokes updaters and the test renderer doesn't, so reviewers must catch it.
|
||||
- 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).
|
||||
- Prototype affordances: implemented or VISIBLY deferred — never silently dropped.
|
||||
- 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).
|
||||
are BARE 4xx/5xx unless the controller returns ProblemDetails); mount call-counts assert
|
||||
the DELTA across navigation; status-dot state never color-only (StatusDot has a label).
|
||||
- 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.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).
|
||||
schedule estimator materializes collections per GET; /api/health TTL cache;
|
||||
`LibraryScanStatusResponseModel.percent` 0–1 under a percent name; no Dialog/Modal
|
||||
component yet (needed by #89); 1 pre-existing --text-faint usage in shell.css.
|
||||
NEW (#129): expose a shared programme/playout-item id on /api/guide + /api/channels/state;
|
||||
extract the duplicated channel-state poll loop (guide.ts copies channels.ts line-for-line)
|
||||
into a shared helper; EPG grid re-renders unmemoized on every 30/60s tick (useMemo the
|
||||
derived grid, isolate the now layer); /api/guide 21-include eager-load still untrimmed.
|
||||
Filed: #126 (OpenAPI polymorphism gap).
|
||||
|
||||
---
|
||||
|
||||
# PROMPT FOR CODEX — #85: Guide / EPG screen (channels × time grid)
|
||||
# PROMPT FOR CODEX — #65: library browse + search API with artwork (first #62 prerequisite)
|
||||
|
||||
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.
|
||||
You are Codex, the IMPLEMENTER, in /Users/timothy/ersatztv (ErsatzTV fork; .NET 10,
|
||||
CQRS/MediatR, LanguageExt, EF Core dual-provider). Fable (Claude) reviews your PR read-only
|
||||
afterwards — do NOT merge. Read CLAUDE.md and docs/contributing.md first; follow the REST
|
||||
API conventions from the #2a foundation (ApiResults, NotFoundError, request DTOs, paged
|
||||
responses — copy the patterns of the existing paged endpoints and their tests, e.g.
|
||||
ScheduleController/PlayoutController + ErsatzTV.Tests controllers/handlers).
|
||||
|
||||
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 ci` in web/ in the worktree BEFORE any lint/typecheck/test/build.
|
||||
mechanical work (DTO/test boilerplate, OpenAPI regen churn); higher-effort agents for
|
||||
judgment work (search-index reuse strategy, projection shape). Keep orchestration and
|
||||
final assembly in your main session.
|
||||
- Worktree: `git worktree add .worktrees/issue-65-library-browse -b feat/65-library-browse origin/main`.
|
||||
Never touch the main checkout or other .worktrees/*. Run `npm ci` in web/ in the worktree
|
||||
if you touch anything web-side (typegen check).
|
||||
- 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!).
|
||||
- BACKEND-only: no web/ feature code (regenerating web/src/api/generated/v1.d.ts to prove
|
||||
typegen still works is fine and encouraged). No DB schema changes expected — this is
|
||||
read-only projections; if you believe you need a migration, STOP and say so on the issue
|
||||
first.
|
||||
- DTO records in ErsatzTV.Core/Api get file-scoped `#nullable enable` (see lessons above).
|
||||
|
||||
## 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.
|
||||
Issue #65 (part of epic #62): browse + search API powering the Channel Builder's (#89) left
|
||||
pane — poster grid, searchable, filterable. Read the FULL issue body incl. the 2026-07-02
|
||||
scope additions. Deliver:
|
||||
- List/search shows, movies, artists, collections across libraries, with paging and text
|
||||
search on title. REUSE the existing server-side search index (ErsatzTV has Lucene-based
|
||||
search infrastructure — explore ISearchIndex/SearchIndex usage in the app + Blazor UI and
|
||||
prefer that over new EF query paths; state your reuse decision on the issue).
|
||||
- Artwork references suitable for a poster grid (match how existing endpoints reference
|
||||
artwork; do not invent a new artwork URL scheme).
|
||||
- Filters: by library and media type; library list for filter chips comes from the
|
||||
media-sources read model (#103) — consume, don't duplicate.
|
||||
- Scope additions: per-item duration + episode/item counts in the browse projection;
|
||||
collection metadata (type badge Manual/Smart/Multi/Rerun, item count, total duration,
|
||||
artwork); typed picker resolution for the schedule editor's Content tab — results must
|
||||
resolve to the correct id type per CollectionType (CollectionId/MultiCollectionId/
|
||||
SmartCollectionId/MediaItemId...). If a scope addition turns out large, implement the
|
||||
core browse/search first and propose a split on the issue rather than silently dropping.
|
||||
- Performance: interactive-typing fast; paged; no unbounded materialization (see the #102
|
||||
lesson — no 21-include eager loads).
|
||||
|
||||
## Context (main = d63719ec; baselines: ErsatzTV.Tests 368, Core.Tests 493+1skip, web 75)
|
||||
## Context (main = 8a62f222; baselines: ErsatzTV.Tests 368, Core.Tests 493+1skip, web 80)
|
||||
- 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/guide?start=<ISO>&end=<ISO> (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.
|
||||
- After code changes: normal `dotnet build ErsatzTV/ErsatzTV.csproj` FIRST, then
|
||||
./scripts/update-openapi.sh (regen is authoritative); commit the regenerated v1.json.
|
||||
- New endpoints need: OpenAPI operation names (route Name=), error-contract test entries
|
||||
(OpenApiErrorResponseContractTests) for 404-able routes, paging clamped (don't repeat the
|
||||
unclamped-pageSize nit), and handler tests via the ErsatzTV.Tests harness precedents.
|
||||
- Consumers to design for (don't implement): #89 Channel Builder poster grid + lineup totals;
|
||||
#86 schedule editor Content-tab picker (typed ids).
|
||||
|
||||
## Process
|
||||
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): Guide / EPG screen (#85)", body lists the mapping table
|
||||
and scope decisions; `closes #85`. Poll CI by head SHA until green. Do NOT merge.
|
||||
1. Comment on issue #65 with findings + approach (search-index reuse decision, projection
|
||||
shape, endpoint list with routes/DTOs) BEFORE coding.
|
||||
2. Implement; comment progress on #65 as you go.
|
||||
3. Verify: TZ=UTC dotnet build ErsatzTV.sln; TZ=UTC dotnet test ErsatzTV.Tests then
|
||||
ErsatzTV.Core.Tests sequentially (expect 368+new / 493+1skip); if v1.json changed, cd web
|
||||
&& npm ci && npm run generate:api && npm run typecheck (generated types must still
|
||||
compile; commit the regenerated v1.d.ts).
|
||||
4. Push, open PR → main: "feat(api): library browse + search endpoints (#65)", body lists
|
||||
endpoints + projection decisions; `closes #65`. 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,
|
||||
files changed, endpoint/DTO 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.
|
||||
THIS handoff (pop #65, next prompt = #64 Channel Templates, record PR + main SHA +
|
||||
baselines), and pushes it to main.
|
||||
|
||||
---
|
||||
|
||||
## Issue queue (work top-down)
|
||||
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: #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.
|
||||
1. #65 library browse + search API ← CODEX PROMPT above (first #62 prerequisite; Fable
|
||||
reviews via subagents, merges on consent, updates this file).
|
||||
2. #62 epic continues: #64 Channel Templates (entity + dual migrations; design early — #63
|
||||
depends on it; investigate #68 alongside) → #63 composite create-channel endpoint →
|
||||
then #89 Channel Builder (also needs #104 artwork upload ✓ done; first Dialog/Modal
|
||||
component; languages endpoint follow-up from #105 when needed). #66/#67 as #89 demands.
|
||||
3. #93 Settings screen is dependency-free — usable as a frontend interleave if a backend
|
||||
session needs review turnaround.
|
||||
4. Then: #90 rebrand → #91 cutover.
|
||||
Cross-refs: #99 seam landed (PR #121), final wiring open. Done recently: PR #128 (#88),
|
||||
PR #129 (#85 Guide/EPG — closed 2026-07-05, main 8a62f222). Filed: #126.
|
||||
|
||||
Reference in New Issue
Block a user