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); all backend gap issues (#100–#111), all SPA screens (#84–#89, #93, #109), the rebrand (#90), the cutover root-flip (#91 phase a), parity pass 2 (#140/#142/#143/#146/#147, PRs #165–#169), and the FULL scheduling parity #144/#162 (PRs #170/#171/#173/#174/#175/#179) are MERGED. v26.5.0 tagged + DEPLOYED to prod 2026-07-07. Blazor removal (#91 phase b) is now gated ONLY on: #145 leftovers, #141 leftovers, and gap issues #151–#153/#155 (collections/playlists CRUD APIs) + #158 items 4–5 + #161 items 3+5.
Session state (2026-07-07 evening, #144 finale): main = c480fbe8, all pre+post-merge CI
green through PR #179. This session merged SIX PRs — the entire #162 slice plan — closing
#144 and #162:
- S5 → PR #170: POST /api/playouts all 5 kinds (scheduleKind discriminator) + PUT /api/playouts/{id} (dailyRebuildTime null-clears, scheduleFile file-kinds-only); AddPlayoutDialog + EditPlayoutDetailsDialog (App.tsx inline). Add Playout button fixed.
- S1 → PR #171: BlockController (groups/blocks CRUD, GET items, PUT full-replace with index-from-array-order, POST preview non-persisting) + GET /api/search/{collections, television-shows,television-seasons,smart-collections} pickers + /app/blocks list+editor.
- S2 → PR #173: TemplateController (+?templateGroupId= filter) + POST .../copy for BOTH templates and blocks + /app/templates (table editor, not Blazor's drag calendar — accepted deviation; client-side overlap+midnight warnings mirror the server).
- S3 → PR #174: DecoController (6-section 22-field PUT; Merge-mode request validation) + PUT /api/playouts/{id}/deco + GET-only PlaylistController (/api/playlists/groups, /api/playlists?playlistGroupId=) + GET /api/search/{artists,multi-collections} + /app/decos. Legacy Artist/MultiCollection break-content renders read-only, round-trips.
- S4 → PR #175: DecoTemplateController + /app/deco-templates (start + duration editor, endTime 00:00:00 = end-of-day, displays 24:00). No copy exists upstream — not a gap.
- S6 → PR #179: GET/PUT /api/playouts/{id}/alternate-schedules (Classic-only) + /api/playouts/{id}/templates (Block-only); index from array order, LAST alt-schedule row = catch-all default (writes playout.ProgramScheduleId; SPA badges it + disables its recurrence controls); PlayoutResponseModel gained decoId/decoName; default-deco select on Block playout cards; playouts route now allowSubPaths via PlayoutsRouteScreen.
- Upstream Application-handler bugs fixed at the root along the way (all surfaced by the new API, all 422-now-not-500): missing group-existence checks in Create{Block,Template, Deco,DecoTemplate}Handler; ReplaceTemplateItems KeyNotFoundException on unknown blockId; midnight-crossing template items accepted-then-silently-dropped; ReplaceDecoTemplateItems silent-drop/no-overlap-check; UpdateDecoHandler dropped MediaItemId on break-content writes (silent corruption); alternate-schedules empty-list Max() throw; out-of-day-range TimeSpans/dates persisted raw then crashed the selector/builder.
- Issues: #144 CLOSED, #162 CLOSED. Filed: #172 (API hardening nits consolidated), #176 (PseudoTV-style channel-first creation rethink — user vision, post-phase-(b)), #177 (Jellyfin music-video Album/Track metadata gap), #178 (far-future Jellyfin plugin idea).
- Tests: ErsatzTV.Tests 621→786, Core.Tests 493 (stable), web 218→301. All slices: adversarial fork review + live browser E2E + should-fixes applied before merge.
- Worktrees all removed except .worktrees/docs-wrapup (this doc commit; remove after).
Lessons for all remaining prompts (accumulated; pruned):
- The main checkout (/Users/timothy/ersatztv) sits on the STALE docs/59-ui-redesign-brief branch. NEVER recon/edit there — point subagents at a worktree pinned to origin/main.
- IMPLEMENTER AGENTS MUST RUN
npm run lintbefore finishing — CI lint failed once (S5) on react-hooks set-state-in-effect. Rule of thumb: NO synchronous setState inside useEffect; derive state or set only in promise callbacks (useChannelsQuery precedent). - SPA sub-path screens MUST own their pathname state + popstate listener (BlocksScreen / PlayoutsRouteScreen pattern). App-level routeFromLocation returns the SAME route object for /app/x and /app/x/{id} (allowSubPaths prefix match), so setActiveRoute bails via Object.is and nothing re-renders — navigateToPath's synthetic popstate is NOT enough.
- Live E2E: copy ErsatzTV/wwwroot INCLUDING app/ into bin/Debug/net10.0/wwwroot AND RESTART (static middleware won't see wwwroot appearing post-startup). UI port 8410, API 8409. Scratch ETV_CONFIG_FOLDER; curl localhost; wait "Done migrating search index"; mutating verbs open without API key. Playwright MCP can only write screenshots into the ROOT checkout (.playwright-mcp) — tolerated exception. Creating a Classic playout via API needs channel enums: ChannelPlayoutSource=Generated, ChannelPlayoutMode=Continuous, ChannelSongVideoMode=Default, ChannelTranscodeMode=OnDemand, ChannelIdleBehavior=StopOnDisconnect.
- WRAPPING CQRS HANDLERS IN REST: check for these latent bug classes and fix at the handler (established convention, ~8 fixes merged): missing FK existence checks (FK violation → unhandled 500), dictionary-indexer lookups (KeyNotFoundException → 500), silent item filtering at persist/read time (reject with 422 instead), unbounded TimeSpan/month/day ints that crash consumers later. Deep FK ids (collection/media refs in items) are deliberately NOT existence-checked (matches schedules precedent) — #172.
- DayOfWeek serializes as day-name STRINGS (StringEnumConverter) though OpenAPI says integer — TS client overrides both directions (playouts.ts WithDayNames precedent).
- PUT-replace list endpoints: index from array order (ReplaceScheduleItemsRequest precedent). Alternate schedules: LAST row = catch-all default (selector is OrderBy(Index) first-match-wins; handler writes Max(Index) row's scheduleId to the playout). GET /api/blocks and /api/templates synthesize negative-id "(none)" rows per empty group — SPA pickers/lists must filter id > 0 (#172 wants them gone at the API).
- PARALLEL/STACKED PARITY BRANCHES CONFLICT on web/src/App.tsx, v1.json/v1.d.ts, and the security/contract test lists. Routine: stack the next slice on the previous branch, and merge origin/main into the branch before PR → union resolve → REGENERATE v1.json + v1.d.ts (authoritative) → full test pass. Stacked merges were conflict-free all session.
- Gitea 1.24 has no rerun-run API; retrigger CI with an empty commit. Poll CI by commit: /api/v1/repos/timothy/ersatztv/commits/{sha}/status.
- ./scripts/update-openapi.sh needs a prior normal dotnet build; web typegen = npm run generate:api. Request DTOs (Controllers/Api/Requests) have no #nullable; response DTOs in ErsatzTV.Core/Api MUST get file-scoped #nullable enable. ErsatzTV.Application has NO nullable context (CS8632). NSubstitute+ConfigElementKey: Arg.Any + generic. LanguageExt: MatchUnsafe for Option→nullable; foreach(x in either.LeftToSeq()) for error extraction.
- Known dead UI nit (multiple screens): the header primaryAction "New Group" button isn't wired on Blocks/Decos-style screens (screen-local buttons work) — fold into a sweep.
- Backlog nits live in #172 (hardening) + #163/#164 (goldens, health UX). Trakt matched-items link → /app/search swap and GET /api/search parallelization still open.
PROMPT — Parity endgame: #145 leftovers (block-playout history, YAML validator), #141 leftovers (media detail pages, image browser) → #91 phase (b) readiness
You are Fable, the ORCHESTRATOR in the main Claude Code session (Claude Code only). Fable is EXPENSIVE: delegate (recon → Explore/haiku; mechanical/reconciles → sonnet; judgment-heavy code → opus; fable forks for review). Read CLAUDE.md + the Lessons above first.
HARD CONSTRAINTS:
- Work in worktrees off origin/main; NEVER touch /Users/timothy/ersatztv (stale branch).
cd web && npm ciin any NEW worktree (or copy node_modules from a sibling worktree). - Max 2–3 concurrent builds; ONE dotnet build at a time. NEVER set ETV_UPDATE_GOLDENS.
- Merge consent in-conversation per PR (user may pre-approve the session). CI reruns = empty commit.
- Live-E2E new screens (Lessons above: wwwroot+app copy + restart, ports, enums).
- Adversarial review fork over each PR diff before merge; apply should-fixes.
- Implementers must run
npm run lint; sub-path screens own pathname state (Lessons).
Known facts (2026-07-07 — re-verify cheaply)
- main =
c480fbe8(post-#179): ALL scheduling editors are in the SPA. Remaining Blazor-only surface (phase (b) gates): #145 leftovers + #141 leftovers + #151–#153/#155. - #145 leftovers (issue open): block-playout history screens (#158 item 4 endpoints: GetAllBlockPlayouts/GetAllBlocksForPlayout/GetBlockPlayoutHistory/DecodePlayoutHistory) [M]; sequential-schedule YAML validator (#158 item 5: POST body, not server path) [S-M]; playback-troubleshooting screen (endpoints live + in spec; needs HLS player + session-log retrieval design) [L — own session, LAST].
- #141 leftovers (issue open): detail endpoints+pages for movie/show/season/artist + GetMediaItemInfo (#161 item 3) [M-L]; image folder browser (#161 item 5) [S-M].
- #151–#153/#155 (collections/playlists CRUD APIs) are NOT in this prompt's scope unless everything above lands with capacity to spare — but S3 already shipped playlist LIST endpoints (PlaylistController) + artist/multi-collection search, which those issues can build on.
- Quick wins interleavable: Trakt matched-items link → /app/search; parallelize GET /api/search (Task.WhenAll); dead header "New Group" primaryAction sweep.
Task
- Verify main green (post-#179 run 596 was green at handoff time). 1b. FIRST: #180 (Channel Builder season-tile flooding + broken artwork) — small, user-visible, diagnosis already in the issue. Same PR routine.
- #145 leftovers: block-playout history [M] first (worktree, recon the 4 CQRS queries + Blazor history pages, thin wrappers + SPA screen), then YAML validator [S-M] (same or second PR). Tests + live E2E, merge main in, PR, fork review, CI, consent, merge. Comment on #145/#158 per landing; close #158 when items 4–5 done (1–3 landed in #165).
- #141 leftovers: media detail endpoints+pages [M-L], then image folder browser [S-M]. Same routine; close #141/#161 when done.
- If #145's playback-troubleshooting is all that remains, leave it for its own session (update #145 accordingly) — do NOT start it late in a session.
- READINESS CHECK once #141 closes and #145 is troubleshooting-only: enumerate remaining Blazor-only routes (grep @page in ErsatzTV/Pages minus migrated), reconcile against #151–#153/#155 + troubleshooting, and file the phase (b) removal plan on #91 (or file what's missing).
- Update THIS handoff (state, lessons, next prompt), commit to main, print next prompt in a fenced block.
Issue queue (work top-down)
- #180 Channel Builder library picker: season tiles flood the grid + ALL artwork broken [S-M, user-visible bug on the flagship flow — diagnosis + fix options in the issue; check CollectionsScreen add-items dialog too; verify prod 26.5.0 vs :latest]
- #145 block-playout history screens [M] ← PROMPT above
- #145 sequential-YAML validator [S-M]
- #141 media detail endpoints+pages [M-L]
- #141 image folder browser [S-M]
- #91 phase (b) readiness check + removal plan (gated also on #151–#153/#155)
- #145 playback troubleshooting [L, own session]
- #151–#153/#155 collections/playlists CRUD APIs (S3's PlaylistController is the seed) Backlog (non-blocking): #172 API hardening, #163 playout goldens, #164 health UX, #176 PseudoTV-style creation rethink (post-phase-(b)), #177 Jellyfin music-video Album/Track, #178 Jellyfin plugin idea, #66 artwork sniffing, search parallelization, Trakt link swap.