Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ filed backend gap issues #100–#111; a 7–9-way parallel workflow build once e
|
||||
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).
|
||||
(PR #128), #85 Guide/EPG (PR #129); #62 prerequisites: #65 library browse (PR #130).
|
||||
|
||||
**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,20 +18,38 @@ 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-05, post-#85)**: main = 8a62f222 (PR #129 merge): Guide/EPG screen
|
||||
live — bounded 13h JSON guide window (never polled; the endpoint is expensive per #102),
|
||||
client-side now marker on a 60s tick, /api/channels/state at 30s for live dots, prev/next/
|
||||
jump-to-now each = exactly one guide fetch, programme clipping at window edges. Fable's
|
||||
8-finder/per-candidate-verifier review found 1 CONFIRMED blocker (crash: `nowPlaying === null`
|
||||
guard vs field OMITTED on the wire → `undefined` → TypeError on any on-air channel playing
|
||||
offline-fallback filler) + 1 CONFIRMED functional bug (live highlight matched on title, but
|
||||
guide titles are show-only vs nowPlaying's `GetDisplayTitle` — could never match episodes);
|
||||
Codex fixed both in a8d4979 (truthiness guard + wire-shape fixture; timestamps-only match)
|
||||
plus 3 cleanups. Deferred cleanups → backlog nits below. #85 CLOSED. Baselines:
|
||||
ErsatzTV.Tests **368**, 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).
|
||||
**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).
|
||||
|
||||
**Lessons for all remaining prompts** (accumulated):
|
||||
- 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
|
||||
@@ -63,110 +81,122 @@ clean. Main checkout sits on docs/59-ui-redesign-brief (fully merged, safe to sw
|
||||
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.
|
||||
- Backlog nits (unfiled): unclamped pageSize; 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` 0–1 under a percent name; no Dialog/Modal
|
||||
component yet (needed by #89); 1 pre-existing --text-faint usage in shell.css.
|
||||
NEW (#129): expose a shared programme/playout-item id on /api/guide + /api/channels/state;
|
||||
extract the duplicated channel-state poll loop (guide.ts copies channels.ts line-for-line)
|
||||
into a shared helper; EPG grid re-renders unmemoized on every 30/60s tick (useMemo the
|
||||
derived grid, isolate the now layer); /api/guide 21-include eager-load still untrimmed.
|
||||
Filed: #126 (OpenAPI polymorphism gap).
|
||||
- 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` 0–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.
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
# PROMPT FOR CODEX — #65: library browse + search API with artwork (first #62 prerequisite)
|
||||
# PROMPT FOR CODEX — #64: Channel Templates (built-in + custom) — first schema change
|
||||
|
||||
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 the existing paged endpoints and their tests, e.g.
|
||||
ScheduleController/PlayoutController + ErsatzTV.Tests controllers/handlers).
|
||||
responses) — 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 (search-index reuse strategy, projection shape). Keep orchestration and
|
||||
final assembly in your main session.
|
||||
- Worktree: `git worktree add .worktrees/issue-65-library-browse -b feat/65-library-browse origin/main`.
|
||||
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`.
|
||||
Never touch the main checkout or other .worktrees/*. Run `npm ci` in web/ in the worktree
|
||||
if you touch anything web-side (typegen check).
|
||||
- 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: no web/ feature code (regenerating web/src/api/generated/v1.d.ts to prove
|
||||
typegen still works is fine and encouraged). No DB schema changes expected — this is
|
||||
read-only projections; if you believe you need a migration, STOP and say so on the issue
|
||||
first.
|
||||
- DTO records in ErsatzTV.Core/Api get file-scoped `#nullable enable` (see lessons above).
|
||||
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.
|
||||
- DTO records in ErsatzTV.Core/Api get file-scoped `#nullable enable`; Application has NO
|
||||
nullable context (`string?` there trips CS8632).
|
||||
|
||||
## Task
|
||||
Issue #65 (part of epic #62): browse + search API powering the Channel Builder's (#89) left
|
||||
pane — poster grid, searchable, filterable. Read the FULL issue body incl. the 2026-07-02
|
||||
scope additions. Deliver:
|
||||
- List/search shows, movies, artists, collections across libraries, with paging and text
|
||||
search on title. REUSE the existing server-side search index (ErsatzTV has Lucene-based
|
||||
search infrastructure — explore ISearchIndex/SearchIndex usage in the app + Blazor UI and
|
||||
prefer that over new EF query paths; state your reuse decision on the issue).
|
||||
- Artwork references suitable for a poster grid (match how existing endpoints reference
|
||||
artwork; do not invent a new artwork URL scheme).
|
||||
- Filters: by library and media type; library list for filter chips comes from the
|
||||
media-sources read model (#103) — consume, don't duplicate.
|
||||
- Scope additions: per-item duration + episode/item counts in the browse projection;
|
||||
collection metadata (type badge Manual/Smart/Multi/Rerun, item count, total duration,
|
||||
artwork); typed picker resolution for the schedule editor's Content tab — results must
|
||||
resolve to the correct id type per CollectionType (CollectionId/MultiCollectionId/
|
||||
SmartCollectionId/MediaItemId...). If a scope addition turns out large, implement the
|
||||
core browse/search first and propose a split on the issue rather than silently dropping.
|
||||
- Performance: interactive-typing fast; paged; no unbounded materialization (see the #102
|
||||
lesson — no 21-include eager loads).
|
||||
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.
|
||||
|
||||
## Context (main = 8a62f222; baselines: ErsatzTV.Tests 368, Core.Tests 493+1skip, web 80)
|
||||
## Context (main = c4007293; baselines: ErsatzTV.Tests 379, 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.
|
||||
- New endpoints need: OpenAPI operation names (route Name=), error-contract test entries
|
||||
(OpenApiErrorResponseContractTests) for 404-able routes, paging clamped (don't repeat the
|
||||
unclamped-pageSize nit), and handler tests via the ErsatzTV.Tests harness precedents.
|
||||
- Consumers to design for (don't implement): #89 Channel Builder poster grid + lineup totals;
|
||||
#86 schedule editor Content-tab picker (typed ids).
|
||||
- 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).
|
||||
|
||||
## Process
|
||||
1. Comment on issue #65 with findings + approach (search-index reuse decision, projection
|
||||
shape, endpoint list with routes/DTOs) BEFORE coding.
|
||||
2. Implement; comment progress on #65 as you go.
|
||||
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.
|
||||
3. Verify: TZ=UTC dotnet build ErsatzTV.sln; TZ=UTC dotnet test ErsatzTV.Tests then
|
||||
ErsatzTV.Core.Tests sequentially (expect 368+new / 493+1skip); if v1.json changed, cd web
|
||||
&& npm ci && npm run generate:api && npm run typecheck (generated types must still
|
||||
compile; commit the regenerated v1.d.ts).
|
||||
4. Push, open PR → main: "feat(api): library browse + search endpoints (#65)", body lists
|
||||
endpoints + projection decisions; `closes #65`. Poll CI by head SHA until green. Do NOT
|
||||
merge.
|
||||
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.
|
||||
|
||||
## 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, endpoint/DTO 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 #65, next prompt = #64 Channel Templates, record PR + main SHA +
|
||||
baselines), and pushes it to main.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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. #65 library browse + search API ← CODEX PROMPT above (first #62 prerequisite; Fable
|
||||
reviews via subagents, merges on consent, updates this file).
|
||||
2. #62 epic continues: #64 Channel Templates (entity + dual migrations; design early — #63
|
||||
depends on it; investigate #68 alongside) → #63 composite create-channel endpoint →
|
||||
then #89 Channel Builder (also needs #104 artwork upload ✓ done; first Dialog/Modal
|
||||
component; languages endpoint follow-up from #105 when needed). #66/#67 as #89 demands.
|
||||
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
|
||||
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. Done recently: PR #128 (#88),
|
||||
PR #129 (#85 Guide/EPG — closed 2026-07-05, main 8a62f222). Filed: #126.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user