Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Fable 5 16674ba80e
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m43s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m22s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m32s
docs: advance ChicoryTV issue queue past #64 (PR #133); next prompt = #63 composite create-channel
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>
2026-07-06 19:45:08 +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).

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):

  • 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; 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/*. Run npm ci in web/ in the worktree if you regen typegen.
  • Max 23 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.csproj FIRST, 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

  1. 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.
  2. Implement; comment progress on #63 as you go.
  3. 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).
  4. 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)

  1. 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).
  2. #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).
  3. #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.
  4. #93 Settings screen is dependency-free — usable as a frontend interleave if a backend session needs review turnaround.
  5. 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 delta 1a61b89f).