17 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: #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-channel — web/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 inlineonClosere-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/browsemediaTypeis 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 generatedIsSystemPlaylist (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).
SelectOneAsyncre-applies.OrderBy(keySelector)INTERNALLY, which REPLACES any ordering the caller composed before it (#133) — never pre-OrderByinto 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 staysErsatzTV.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/takethreading). 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
*MetadataDbSet 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.Ignoreglobally, so any null DTO property is ABSENT from the JSON →undefinedin 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.
percentis 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.
- 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.percent0–1 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). Thengit 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 cifirst. - Max 2–3 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
- 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).
- Reuse: Dialog/ConfirmDialog (components/overlay.tsx), routing.ts navigateToPath, the established query-hook screen pattern, .ctv-lift/.ctv-press.
- The
settingsroute already exists in App.tsx routes (placeholder) — wire like #89 did. - Tests extend mockDashboardApi in App.test.tsx (baseline 112) per Lessons (fixture fidelity, omitted null keys, honest failure modes).
- PR → main: "feat(web): Settings screen (#93)",
closes #93; poll CI by head SHA; ask "merge?"; verify main post-merge run. - 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)
- 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.
- #93 Settings ← PROMPT above (design-first via Claude Design + /design-sync; scope with
the user;
settingsroute placeholder already exists). - #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).