Also: record prod cutover as done in CLAUDE.md (fork :prod live since 2026-06-27) and add standing v26.4.0 release-checkpoint note to the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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).
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).
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.
Session state (2026-07-06, post-#64): main = 4854c45a (PR #133 merged): ChannelTemplate
domain entity — composition-by-reference (FFmpegProfileId + optional watermark/fallback/
pre/mid/post-roll filler refs) plus stream/audio/playout/subtitle/music-video/transcode/idle/
schedule defaults; IsSystem built-ins ("Standard", "Music videos") seeded idempotently in
DbInitializer (user edits survive restarts, verified by test); default-template selection via
ConfigElement channel_templates.default_template_id; 7 endpoints under /api/channel-templates
(list/get/get-default/set-default/create/update/delete; delete rejects system + active
default). FIRST FORK SCHEMA CHANGE: dual migrations AddChannelTemplates (SQLite
20260706155532 / MySQL 20260706155538) + first IDesignTimeDbContextFactory
(ErsatzTV/TvContextDesignTimeFactory.cs — lets dotnet ef run without a live MySQL).
Fable review found 3 CONFIRMED (untrimmed-name uniqueness check → DbUpdateException/500;
dead .OrderBy(Name) before SelectOneAsync in the default fallback; flat folder layout vs
contributing §2) + nits; Codex fixed all in 1a61b89f (+ shared ChannelTemplateDefault helper,
ConfigElementKey regrouped); deferred: Validation.Apply conversion (would erase NotFoundError
→ breaks 404 mapping, stays early-return Option). #64 CLOSED. Templates are
applied at channel-create time by #63 — NOT live-linked to channels (so #68 unblocked).
Baselines: ErsatzTV.Tests 419, Core.Tests 493 (+1 skip); web tests 80. Main
checkout sits on docs/59-ui-redesign-brief; .worktrees/issue-64-channel-templates is merged
(remove it).
Lessons for all remaining prompts (accumulated):
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; no Dialog/Modal component yet (needed by #89); 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. Filed: #126 (OpenAPI polymorphism gap).
PROMPT FOR CODEX — #63: composite "create channel from lineup" endpoint
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) — copy the patterns of existing controllers/handlers and their tests.
HARD CONSTRAINTS:
- USE SUBAGENTS where appropriate, at fitting effort and model levels: cheap/fast agents for mechanical work (DTO/test boilerplate, OpenAPI regen churn); higher-effort agents for judgment work (transaction orchestration, template-defaults stamping).
- Worktree:
git worktree add .worktrees/issue-63-composite-create -b feat/63-composite-create origin/main. Never touch the main checkout or other .worktrees/*. Runnpm ciin web/ in the worktree if you regen typegen. - 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.
- BACKEND-only; regenerating web/src/api/generated/v1.d.ts to prove typegen still works is fine and encouraged.
- NO schema change expected — this composes EXISTING entities. If you believe you need a migration, STOP and justify on the issue first.
- DTO records in ErsatzTV.Core/Api get file-scoped
#nullable enable; Application has NO nullable context. Command/query files go in<Domain>/Commands|Queries/subfolders.
Task
Issue #63 (part of epic #62; the backend contract for the #89 Channel Builder): ONE API operation that atomically creates everything a working channel needs. Read the FULL issue body first. Deliver:
- Accept: channel basics (name, number, group, image?, always-on?),
templateId(from #64's /api/channel-templates; default template pre-selected client-side) + optional "Advanced" overrides for the template's knobs, shuffle/order mode, and an ORDERED lineup of library item references (ids as produced by #65's /api/library/browse picker ids — movies, shows, seasons, artists, collections, playlists; decide + document which kinds v1 accepts). - Create, in ONE transaction: Channel + Collection (from the lineup) + ProgramSchedule + schedule items + Playout. Roll back cleanly on ANY failure — no orphan entities. State your transaction strategy in the design comment (existing handlers SaveChanges eagerly — you will likely need a single-handler orchestration inside one TvContext transaction rather than chaining existing MediatR commands; check how PlayoutBuilder/rebuild is normally triggered post-create and whether it belongs inside or after the transaction).
- Apply the selected ChannelTemplate's defaults at create time (stamp values — NOT a live link), with Advanced overrides winning over template values. Missing/deleted templateId → 404 via pre-check; invalid lineup ids → 404/422 with the item spelled out.
- Validation per the #133 lessons: normalize name ONCE and check uniqueness against what you persist; channel-number collision → clean 422.
- Response: the created channel (existing ChannelResponseModel or a small composite response with created ids — decide, justify, keep it #89-friendly).
- Route Name= on the endpoint; OpenApiErrorResponseContractTests + ApiErrorResponseMetadataTests entries for every documented 404/422; regenerate v1.json + v1.d.ts.
- DESIGN FOR (don't implement): #89 wizard flow (template select → lineup pick → advanced overrides → create); #71 (persistent shuffle) and #77 (clock-boundary padding) may add knobs later — keep the request shape extensible.
Context (main = 4854c45a; baselines: ErsatzTV.Tests 419, Core.Tests 493+1skip, web 80)
- Gitea: http://192.168.1.95:3000/timothy/ersatztv (basic auth timothy:ded89Lm4).
- After code changes: normal
dotnet build ErsatzTV/ErsatzTV.csprojFIRST, then ./scripts/update-openapi.sh (regen is authoritative); commit the regenerated v1.json. - Study the Blazor create flows for what "a working channel" minimally needs (Channel editor, Schedule editor, Playout add) — the composite must produce a channel that actually plays.
- Templates: GET /api/channel-templates/{id} + ChannelTemplate entity (see PR #133) — fields
cover FFmpeg profile, watermark, fillers, stream/audio/subtitle/music-video/transcode/idle
- schedule defaults (shuffle etc.).
- Test with the ErsatzTV.Tests harness precedents (NUnit + Shouldly + NSubstitute; SQLite in-memory harness). Add a rollback test (fail mid-transaction → NOTHING persisted).
Process
- Comment on issue #63 with findings + approach (request/response DTO shape, accepted lineup-id kinds, transaction strategy, playout-build timing, template stamping matrix) BEFORE coding.
- Implement; comment progress on #63 as you go.
- Verify: TZ=UTC dotnet build ErsatzTV.sln; TZ=UTC dotnet test ErsatzTV.Tests then ErsatzTV.Core.Tests sequentially (expect 419+new / 493+1skip); if v1.json changed, cd web && npm ci && npm run generate:api && npm run typecheck (commit v1.d.ts).
- Push, open PR → main: "feat(api): composite create-channel endpoint (#63)", body lists
DTO shape + transaction strategy + template-stamping decisions;
closes #63. 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, DTO/endpoint 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 #63, next prompt = #89 Channel Builder, record PR + main SHA + baselines), pushes it to main, AND raises the release checkpoint (see standing note above): with #62 complete the API surface for #89 is done — ask the user whether to tag v26.4.0 now or wait for the SPA cutover (#91).
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. Six Renovate/ dependency PRs are open (#21, #48, #49, #61, #131 security, #132) — cheap batch-merge pass when convenient. MCP PR #76 (#58 read-only server foundation) predates most of the API surface — needs a rebase/refresh pass; good parallel track once the API stops moving (post-#63).
- #63 composite create-channel endpoint ← CODEX PROMPT above (last piece of epic #62; unblocks #89; Fable reviews via subagents, merges on consent, updates this file, raises the v26.4.0 release checkpoint).
- #89 Channel Builder (needs #63 + #64 ✓ + #65 ✓ + #104 artwork upload ✓; first Dialog/Modal component; languages endpoint follow-up from #105 when needed). #66/#67 as #89 demands.
- #93 Settings screen is dependency-free — usable as a frontend interleave if a backend session needs review turnaround.
- Then: #90 rebrand → #91 cutover (+ tag v26.4.0 at the latest here — see RELEASE
CHECKPOINT note).
Cross-refs: #99 seam landed (PR #121), final wiring open; #68 unblocked (templates stamp at
create time, no live link). Done recently: PR #130 (#65 library browse), PR #133 (#64
channel templates — closed 2026-07-06, main
4854c45a, fix delta1a61b89f).