docs: advance ChicoryTV issue queue past #64 (PR #133); next prompt = #63 composite create-channel
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

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>
This commit is contained in:
2026-07-06 19:45:08 +02:00
co-authored by Claude Fable 5
parent 4854c45a89
commit 16674ba80e
2 changed files with 121 additions and 92 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ Custom IPTV channel server for Jellyfin. Forked from [ErsatzTV/ErsatzTV](https:/
- **Docker host**: jazz (192.168.1.99), container `ersatztv`, port 8409
- **Config volume**: `~/downloadswarm/ersatztv/` on jazz → `/config` in container
- **SQLite DB**: `/config/ersatztv.sqlite3` (WAL mode, root-owned)
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml``192.168.1.95:3000/timothy/ersatztv`): push to `main``:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Prod container still runs upstream `ghcr.io/ersatztv/ersatztv:latest` pending cutover (server-management#481). Pipeline details: `docs/ci-cd.md`.
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml``192.168.1.95:3000/timothy/ersatztv`): push to `main``:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Cutover done (2026-06-27, server-management#481/#482): prod container runs fork `:prod` (currently v26.3.1), test container tracks `:latest`; prod advances only when a new `v*` tag is pushed (next: `v26.4.0`, first app-change release). Pipeline details: `docs/ci-cd.md`.
## Development
+120 -91
View File
@@ -7,7 +7,8 @@ filed backend gap issues #100#111; a 79-way parallel workflow build once e
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).
(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 +
@@ -18,23 +19,44 @@ subagents too (NITS: Fable fixes on the branch; SUBSTANTIAL: back to Codex verba
in-session — user decides). Merges need explicit user consent (standing consent for this
stretch: merge when reviewers are happy and tests pass).
**Session state (2026-07-06, post-#65)**: main = c4007293 (PR #130): `GET /api/library/browse`
(`BrowseLibrary`) — unified browse/search over movies/shows/seasons/artists via the existing
Lucene ISearchIndex plus manual/smart/multi/rerun collections + playlists via narrow EF
projections (they are not Lucene docs), merged paging with a skip cascade, typed picker ids
per CollectionType, per-item duration + counts, poster artwork via the existing
jellyfin://-emby:// proxy conventions, manual-collection total duration + representative
poster, pageNum/pageSize clamped. Fable's 3-lens review (correctness / API-contract /
tests+scope) found 2 SUBSTANTIAL (manual-collection artwork+duration silently swept into a
deferral that only covered smart/multi/rerun; tests missed exactly the risky logic —
cross-source paging, season/artist hydration, libraryId exclusion rule) + 6 nits; Codex fixed
ALL in 630119f3, delta re-verified by Fable. #65 CLOSED. Deferred on #65: exact aggregate
duration/artwork for smart/multi/rerun (kind badge + item counts only until #89 needs more).
Baselines: ErsatzTV.Tests **379**, Core.Tests **493** (+1 skip); web tests **80**;
lint/typecheck/build clean. Main checkout sits on docs/59-ui-redesign-brief (fully merged,
safe to switch).
**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<BaseError>). #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.
@@ -81,6 +103,8 @@ safe to switch).
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
@@ -89,114 +113,119 @@ safe to switch).
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.
NEW (#130): the two manual-collection metadata helpers each fetch CollectionItems (share
one fetch); very large manual collections make the browse duration sum heavy (bounded per
page but per-item MediaVersions loads). Filed: #126 (OpenAPI polymorphism gap).
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 — #64: Channel Templates (built-in + custom) — first schema change
# 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, paged
responses) — copy the patterns of existing controllers/handlers and their tests.
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, migration scaffolding checks);
higher-effort agents for judgment work (template composition model, seeding strategy).
Keep orchestration and final assembly in your main session.
- Worktree: `git worktree add .worktrees/issue-64-channel-templates -b feat/64-channel-templates origin/main`.
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 touch anything web-side (typegen check).
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: no web/ feature code (regenerating web/src/api/generated/v1.d.ts to prove
typegen still works is fine and encouraged).
- THIS ISSUE ADDS A DOMAIN ENTITY → migrations in BOTH providers via
`scripts/add-migration.sh <Name>` (does SQLite + MySql together; NEVER hand-write or
single-provider). CI's `migrations` job enforces model-drift + apply-to-fresh-DB per
provider — a red migrations job means your model and migration disagree.
- 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 (`string?` there trips CS8632).
nullable context. Command/query files go in `<Domain>/Commands|Queries/` subfolders.
## Task
Issue #64 (part of epic #62; directly serves the #59 goal "better defaults and templates"):
a **ChannelTemplate** entity bundling the clunky technical/behavioral channel settings so
most channels need zero manual config. Read the FULL issue body first. Deliver:
- Domain entity + dual migrations. Design decision to state on the issue BEFORE coding:
compose existing entities by REFERENCE (FFmpegProfileId, FillerPresetId for pre/mid/
post-roll, watermark?) vs duplicating values — strongly prefer composition-by-reference;
survey what a channel + playout actually needs configured (Channel entity, ProgramSchedule
defaults, subtitle/shuffle/playout knobs) and pick the bundle deliberately. Fields the
issue names: transcode/FFmpeg profile, fillers, interleaving, subtitle behavior, shuffle
default, playout defaults.
- **Built-in templates** shipped out of the box (e.g. "Standard", "Music videos") AND
user-saved custom templates. Investigate how the codebase seeds defaults today (e.g. the
default FFmpegProfile) and follow that precedent; built-ins should be non-deletable (or
clearly flagged `IsSystem`) and survive upgrades without clobbering user edits.
- A way to mark/get the DEFAULT template (pre-selected in the create flow).
- REST CRUD: list/get/create/update/delete under /api/channel-templates (+ default
selection). Deleting a template must not break channels created from it (templates are
applied at create time by #63, not live-linked — state this explicitly in your design
comment if you agree, or argue otherwise). 404s via pre-check + ApiResults with
OpenApiErrorResponseContractTests entries; route Name= on every endpoint; clamp paging if
you page.
- DESIGN FOR (don't implement): #63's composite create-channel endpoint consumes a template
+ "Advanced" overrides; the SPA create flow (#89) lists templates with the default
pre-selected. Keep the DTO shape friendly to both.
- If the bundle turns out to have a large tail of fields, land the entity + CRUD + a solid
core bundle first and propose the tail as a follow-up on the issue rather than gold-plating.
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 = c4007293; baselines: ErsatzTV.Tests 379, Core.Tests 493+1skip, web 80)
## 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.
- Related open issue #68 (resume/bookmark channels) may eventually want per-channel behavior
flags — do NOT implement, but note in your design comment if the template shape would
block or ease it.
- Test with the ErsatzTV.Tests harness precedents (NUnit + Shouldly + NSubstitute;
InMemoryTvContext for handler tests).
- 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 #64 with findings + approach (entity shape, composition-by-reference
decision, seeding strategy for built-ins, endpoint list with routes/DTOs) BEFORE coding.
2. Implement; comment progress on #64 as you go.
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 379+new / 493+1skip); migrations: apply both
providers' migrations to a fresh DB if feasible locally (SQLite at minimum) and ensure no
model drift; if v1.json changed, cd web && npm ci && npm run generate:api && npm run
typecheck (commit the regenerated v1.d.ts).
4. Push, open PR → main: "feat(api): channel templates entity + CRUD (#64)", body lists
entity design + endpoints + seeding decisions; `closes #64`. Poll CI by head SHA until
green — the `migrations` job especially. Do NOT merge.
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, entity/DTO/endpoint table, migration names, 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 #64, next prompt = #63 composite create-channel
endpoint, record PR + main SHA + baselines), and pushes it to main.
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)
0. HOUSEKEEPING: #99 stays open for the final /api/channels/state onAir wiring; #126
(OpenAPI polymorphism gap) is a good backend slot-filler between screens.
1. #64 Channel Templates ← CODEX PROMPT above (first schema change of the fork; #63 depends
on it; Fable reviews via subagents, merges on consent, updates this file).
2. #62 epic continues: #63 composite create-channel endpoint (consumes #64 templates + #65
browse) → then #89 Channel Builder (needs #104 artwork upload ✓, #65 browse ✓, first
(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).
1. #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).
2. #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.
3. #93 Settings screen is dependency-free — usable as a frontend interleave if a backend
session needs review turnaround.
4. Then: #90 rebrand → #91 cutover.
Cross-refs: #99 seam landed (PR #121), final wiring open; #68 reframed (resume/bookmark) —
noted in the #64 prompt as design-awareness only. Done recently: PR #129 (#85 Guide/EPG),
PR #130 (#65 library browse — closed 2026-07-06, main c4007293). Filed: #126.
4. 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).