Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Fable 5 8a323f98bf
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m1s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
docs: advance ChicoryTV issue queue past #89 (PR #136); next prompt = #93 Settings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 23:43:42 +02:00

17 KiB
Raw Blame History

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 79-way parallel workflow build once exhausted RAM, so builds are limited to 23 concurrent, never wide fan-outs. Backend gaps all landed by 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); #62 prerequisites: #65 library browse (PR #130), #64 channel templates (PR #133), #63 composite create-channel (PR #134) — epic #62 COMPLETE; #89 Channel Builder (PR #136) — the flagship screen is live.

PROCESS (2026-07-06, binding — supersedes 07-05): Claude Code ONLY — Codex is retired (usage exhausted). Fable is the orchestrator in the main session and is EXPENSIVE — use it SPARINGLY: delegate implementation to the best-fitting subagent models (haiku for mechanical churn, sonnet for standard components/tests, opus for judgment-heavy logic/orchestration code; fable only for the hardest design calls and the final review fork). Reviews stay multi-lens via subagents (fable correctness fork + cheaper contract/tests lens + a design-system lens for frontend work), plus a fork verification pass over any fix diff. Review fixes are applied by fitting subagents, never inline. npm ci in each fresh worktree before web/ verification. Merges need explicit user consent per PR — NOTE: the permission classifier requires consent IN-CONVERSATION; the standing consent written here does not satisfy it, so ask a quick merge question each time (learned #134). Subagents killed by transient API errors CAN be resumed via SendMessage with their agentId — resume instead of relaunching (their edits are saved; learned #89).

RELEASE CHECKPOINT (standing, added 2026-07-06): prod cutover to the fork is DONE — prod container ersatztv on bumblebee runs 192.168.1.95:3000/timothy/ersatztv:prod (= v26.3.1, app-identical to upstream 26.3.0); ersatztv-test tracks :latest (main). Prod only advances on v* tags. At every milestone merge, FLAG THE USER: is this slice worth tagging v26.4.0 (reserved for the first app-change release)? Latest sensible tag point is #91 (cutover); earlier if a stable API slice should reach prod sooner. Tagging needs explicit user consent; NEVER [skip ci] a commit you'll tag. (Flagged again at the #89 merge, 2026-07-06 — the full builder + backend API is a plausible v26.4.0 slice.)

Session state (2026-07-06, post-#89): main = e1860785 (PR #136 merged): the Channel Builder is live at /app/new-channelweb/src/builder/ChannelBuilder.tsx (~1,300 lines, own module; App.tsx only wires the route), first Dialog/ConfirmDialog primitive in web/src/components/overlay.tsx, new api modules libraryBrowse / channelTemplates (default 404→null) / artwork (multipart /api/artwork/uploads) / pickers (client-sorted filler/watermark/graphics/ffmpeg-profile lists — pickers.ts now OWNS the filler/watermark fetchers; schedules.ts imports them), createChannelFromLineup in channels.ts, web/src/routing.ts (shared navigateToPath, breaks the App↔screen import cycle — reuse it for any new screen module). Shared motion utilities .ctv-lift/.ctv-press now in shell.css. Review: 3 lenses → 1 BLOCKER + 4 SUBSTANTIAL, fixed in 54a0760f, fork-verified SHIP. Follow-up filed: #135 (from-lineup advanced overrides can't express clear-to-none; null = inherit — the UI ships "Inherit from template" pickers until then). Baselines: ErsatzTV.Tests 447, Core.Tests 493 (+1 skip) — untouched (frontend-only); web tests 112 (was 80). Main checkout sits on docs/59-ui-redesign-brief; the .worktrees/issue-89-* worktree was switched to main for this doc commit — REMOVE it at next session start (git worktree remove .worktrees/issue-89-channel-builder).

Lessons for all remaining prompts (accumulated):

  • NEW (#89) — Dialog/portal components: key open-effects on [open] ONLY and read callbacks through a latest-ref; an effect depending on an inline onClose re-runs (and re-focuses) on every parent render — the focus-steal makes dialog inputs untypeable, and jsdom tests can't catch it (fireEvent.change needs no focus).
  • NEW (#89) — before offering a "None"/clear affordance for any field the backend resolves with x ?? fallback, check whether null actually MEANS clear — for from-lineup advanced overrides null = INHERIT (see #135), so honest UI is "Inherit from template", not "None".
  • NEW (#89) — <label onClick={...}> wrapping a labelable control (button/input) double-fires in real browsers (label activation forwarding + bubble); jsdom does not emulate it, tests stay green. Use a <div> row with the control as the single accessible element.
  • NEW (#89) — /api/library/browse mediaType is single-valued: a Collections-style picker needs 5 typed parallel calls (Collection/Smart/Multi/Rerun/Playlist) merged client-side. ApiResults 422 title is ALWAYS "Validation failed" — fixtures must not invent titles.
  • Multiple Dynamic-start Flood schedule items are NON-VIABLE (#134): PlayoutModeSchedulerFlood only yields to a next item with StartType.Fixed (PlayoutModeSchedulerFlood.cs:50-53) and never advances on the hard stop — an ordered multi-source lineup must be ONE generated IsSystem Playlist (PlayAll=true per entry, entries in Index order) behind a single Flood item. PlaylistItem supports Movie/Show/Season/Artist/Collection/Smart/Multi but has NO RerunCollectionId and NO nested-playlist support (CollectionKey.ForPlaylistItem + MediaCollectionRepository.GetPlaylistItemMap are the two switches that define support).
  • Validation must see the SAME data the build path uses (#134): normalizing on a with {} copy inside the validator let raw request values reach persistence (FK violation → opaque 422). Normalize the whole input once up front; both validation and build consume the normalized form.
  • Any handler that SYNTHESIZES names into unique-indexed columns needs de-collision (" 2", " 3"…, max-length-safe) — deleting a channel doesn't cascade its generated schedule/playlist, so recreate-after-delete is a routine path, not an edge case (#134).
  • SelectOneAsync re-applies .OrderBy(keySelector) INTERNALLY, which REPLACES any ordering the caller composed before it (#133) — never pre-OrderBy into SelectOneAsync; write the explicit .Where(...).OrderBy(...).FirstOrDefaultAsync(...) when ordering matters.
  • Normalize user input ONCE (#133): validate uniqueness/lengths against the SAME normalized (e.g. trimmed) value you persist, or a whitespace variant slips past validation and dies on the unique index as an unhandled 500.
  • Application command/query records + handlers live in <Domain>/Commands/ and <Domain>/Queries/ subfolders (contributing §2); namespace stays ErsatzTV.Application.<Domain> regardless of subfolder (#133).
  • Deferral wording must ENUMERATE what is deferred (#130): "aggregate collection metadata is deferred" quietly swallowed manual collections, which are a cheap direct join — the review had to split the deferral. Cheap-vs-expensive is per collection kind, not per feature.
  • Merged-source paging pattern (#130): Lucene supplies media ids+total, EF supplies collection-likes; page = media first, then a skip cascade through each collection type (remainingSkip/take threading). Stale Lucene entries can drift collection paging for a scan window — accepted, documented in-code. Any similar dual-source endpoint should copy the GetLibraryBrowseItemsHandler pattern AND its multi-type-overflow paging test.
  • User text into BOTH Lucene and SQL needs per-side treatment (#130): raw query text is the established Lucene idiom (parser falls back to escaped-literal on ParseException — malformed input degrades to empty/literal results, never throws), but the same text in EF LIKE needs %/_/escape-char escaping or semantics diverge between the two halves.
  • Direct *Metadata DbSet queries need a deterministic winner (#130): items can carry >1 metadata row; either go through the navigation + HeadOrNone() idiom or GroupBy(itemId).OrderBy(Id).First().
  • 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. percent is a 01 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 pendingactive 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.
  • Actionable = visible (#84); disable all mutation triggers while mutating; ref-based double-submit guards; mutations never refetch the world (#125/#127).
  • Honest tests: no scenarios the backend can't produce (no exception middleware → failures 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. Validation.Apply ERASES NotFoundError subtypes (#44 gotcha) — multi-check validation that must 404 stays early-return.
  • Backlog nits (unfiled): unclamped pageSize (browse is clamped; older endpoints aren't); 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; LibraryScanStatusResponseModel.percent 01 under a percent name; 1 pre-existing --text-faint usage in shell.css. From #129: shared programme/playout-item id on /api/guide + /api/channels/state; extract the duplicated channel-state poll loop into a shared helper; EPG grid re-renders unmemoized on every tick; /api/guide 21-include eager-load untrimmed. From #130: the two manual-collection metadata helpers each fetch CollectionItems (share one fetch); very large manual collections make the browse duration sum heavy. From #134: pre-existing non-system PlaylistGroup named "Channel Lineups" breaks multi-item creates with a generic 422; non-DbUpdateException create failures surface as bare 500. From #89: undefined-vs-null lineup keys in the create body (JSON.stringify drops undefined keys; server model-binding treats missing as null — fine today, worth a type tidy); reduced-motion block lists a now-no-op .ctv-builder-libcard. Filed: #126 (OpenAPI polymorphism gap), #135 (clear-to-none).

PROMPT — #93: ChicoryTV Settings screen (prototype → implement)

You are Fable, the ORCHESTRATOR in the main Claude Code session (Codex is retired — Claude Code only). Fable is EXPENSIVE: delegate implementation to fitting subagents (sonnet for components/tests/wiring, haiku for mechanical churn, opus for judgment-heavy parts); reserve fable for design calls and the final review fork. Read CLAUDE.md and the PROCESS + Lessons sections of this file first.

HARD CONSTRAINTS:

  • FIRST: git worktree remove .worktrees/issue-89-channel-builder (left on main by the #89 doc commit). Then git worktree add .worktrees/issue-93-settings -b feat/93-settings origin/main; never touch the main checkout (it sits on docs/59-ui-redesign-brief). cd web && npm ci first.
  • Max 23 concurrent builds; ONE dotnet build at a time. This issue is frontend-first; small backend read/write gaps are plausible (settings endpoints) — scope them BEFORE building UI and reassess with the user if they exceed a session.
  • NEVER set ETV_UPDATE_GOLDENS.
  • Review before PR: parallel lenses (fable correctness fork + sonnet contract/tests + design lens), fixes via subagents, fork verification over the fix diff. Merge needs an in-conversation consent question.

Task

Issue #93: the Settings screen is NOT yet designed — it's the first screen to exercise the design-first workflow: prototype in Claude Design, pull via /design-sync (#92 workflow; localDir = design-system/; the DesignSync MCP only works from the MAIN session — do NOT delegate the sync itself), then implement in the SPA. Scope TBD per the issue (likely: general/server settings, streaming/FFmpeg defaults, media sources, playout defaults, XMLTV/M3U output, appearance/theme, auth/API keys) — define during prototyping WITH the user. Acceptance: prototyped in Claude Design AND implemented via the design-sync workflow.

Approach notes

  1. SCOPE FIRST: inventory what the REST API actually exposes for settings today (ffmpeg-profiles CRUD, watermarks/fillers lists, health, version; check what Blazor's Settings pages edit vs what the API can write) — the screen can only edit what the API can write; read-only display + "managed in legacy UI" callouts are acceptable visible deferrals. Comment the proposed scope on #93 and get user sign-off on scope + design direction BEFORE implementing (this issue is interactive by nature — the user prototypes in Claude Design).
  2. Reuse: Dialog/ConfirmDialog (components/overlay.tsx), routing.ts navigateToPath, the established query-hook screen pattern, .ctv-lift/.ctv-press.
  3. The settings route already exists in App.tsx routes (placeholder) — wire like #89 did.
  4. Tests extend mockDashboardApi in App.test.tsx (baseline 112) per Lessons (fixture fidelity, omitted null keys, honest failure modes).
  5. PR → main: "feat(web): Settings screen (#93)", closes #93; poll CI by head SHA; ask "merge?"; verify main post-merge run.
  6. Update THIS handoff: pop #93, next = #90 rebrand per the queue; record PR + main SHA + baselines. Commit to main. Print the next prompt in a fenced block.

Issue queue (work top-down)

  1. HOUSEKEEPING: remove .worktrees/issue-89-channel-builder (on main, doc-commit leftover). #99 stays open for the final /api/channels/state onAir wiring; #126 (OpenAPI polymorphism)
    • #135 (advanced clear-to-none) are backend slot-fillers between screens. Renovate/dep PRs (#21, #48, #49, #61, #131 security, #132) — cheap batch-merge pass when convenient. MCP PR #76 (#58) still needs its rebase/refresh pass — good parallel track.
  2. #93 Settings ← PROMPT above (design-first via Claude Design + /design-sync; scope with the user; settings route placeholder already exists).
  3. #90 rebrand → #91 cutover (+ tag v26.4.0 at the latest here — see RELEASE CHECKPOINT; flagged again at the #89 merge: the full builder + backend API is already a plausible v26.4.0 slice if the user wants it on prod sooner). Cross-refs: #66/#67 remain open image-pipeline nice-to-haves (the builder ships with client-side checks + single-image logo/bug); #68 unblocked-independent. Done recently: PR #130 (#65), PR #133 (#64), PR #134 (#63), PR #136 (#89 Channel Builder — closed 2026-07-06, main e1860785, review-fix delta 54a0760f; web tests 80 → 112).