Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Fable 5 9497df0d53
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m34s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m54s
docs: advance ChicoryTV issue queue past #93 (PR #138); document design-sync workflow (#92); next prompt = #90 rebrand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:39:27 +02:00

19 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 (PR #130), #64 (PR #133), #63 (PR #134) — epic #62 COMPLETE; #89 Channel Builder (PR #136); #93 Settings (PR #138) — first screen through the full design-first workflow; #92 design-sync round-trip verified and documented (docs/design-sync.md).

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). NEW (#93): LIVE-E2E the new screen against a real fresh local server BEFORE the review lenses — it caught two ship-blockers jsdom can't see (see Lessons: local-run recipe).

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 #93 merge, 2026-07-07 — builder + settings + full API is a very plausible v26.4.0 slice; #90 rebrand would make it present as ChicoryTV.)

Session state (2026-07-07, post-#93): main = 17f49304 (PR #138 merged): Settings is live at /app/settings/<section> — backend GET/PUT /api/settings/{ffmpeg,playout,xmltv, scanner,logging,ui,hdhr} + /api/settings/resolutions CRUD (SettingsController, DTOs in ErsatzTV.Core/Api/Settings), frontend web/src/screens/SettingsScreen.tsx + web/src/api/settings.ts (tiered loader: settings groups gate the screen, reference data via allSettled with per-resource failure notes). App.tsx ScreenRoute gained an opt-in allowSubPaths flag (settings only); the screen parses its own URL suffix. Design-first workflow proven end-to-end: prototype design-system/templates/chicorytv-admin/Settings.jsx

  • handoff bundle design-system/design_handoff_settings/ synced both directions with the DesignSync MCP (main session only) — workflow documented in docs/design-sync.md, #92 CLOSED. Also this session: PR #137 — Scriban.Signed 6.5.2→7.2.5 (GHSA-5wr9-m6jw-xx44 sandbox escape; the fresh advisory made NuGetAudit fail EVERY CI restore, merged first to unblock); media-sources fresh-DB 500 fixed (Dapper→EF, see Lessons). Review: 3 lenses → 11 findings (2 backend contract clusters + frontend silent-failure/partial-save cluster), fixed by 2 parallel subagents, fork-verified SHIP. Baselines: ErsatzTV.Tests 495, Core.Tests 493 (+1 skip), web tests 145 (was 112). All #93 worktrees removed; main checkout still sits on docs/59-ui-redesign-brief — do NOT touch it.

Lessons for all remaining prompts (accumulated):

  • NEW (#93) — Local live-E2E recipe: npm run build (outputs to gitignored ErsatzTV/wwwroot/app/), then ln -sfn <worktree>/ErsatzTV/wwwroot/app ErsatzTV/bin/Debug/net10.0/wwwroot/app (Program.cs sets ContentRoot to the ASSEMBLY dir, so the SPA static-file provider reads bin's wwwroot — publish/Docker copy it, dotnet run doesn't), then ETV_CONFIG_FOLDER=<scratch> ASPNETCORE_URLS=http://127.0.0.1:8409 dotnet run --project ErsatzTV. Fresh DB migrates in seconds; ffmpeg autodetected from PATH. update-openapi.sh FAILS while an instance runs (single-instance mutex) — kill it first.
  • NEW (#93) — Dapper + Microsoft.Data.Sqlite infers expression columns (COUNT(*)) as BLOB when the result set is EMPTY → incompatible deserializer → 500 on every fresh DB. Prefer EF LINQ GroupBy for aggregates in Api handlers; regression-test the empty-DB path.
  • NEW (#93) — Screen hooks must TIER their loads: the screen's own resources gate loading/error; reference data (pickers, sources, health, version) settles per-resource (allSettled) with inline "Couldn't load X" notes. Precedent: settings.ts. Also: render the error branch BEFORE the loading branch — a draft-null loading guard ahead of the error check made the error state unreachable (infinite spinner).
  • NEW (#93) — ApiResults maps ONLY NotFoundError→404; plain BaseError→422. Handlers that collapse "missing" and "invalid state" into one query filter can't 404 — split the lookup (precedent: DeleteCustomResolutionHandler). Request DTOs in Controllers/Api/Requests deliberately have NO #nullable enable (only RESPONSE DTOs get it). Startup's UseStringEnumSchemas registers non-Core enums (OutputFormatKind, LogEventLevel) INDIVIDUALLY — assembly-wide reflection over ErsatzTV.FFmpeg throws ReflectionTypeLoadException (optional NvEncSharp natives). Core↔Application enum twins bridge via exhaustive switch expressions, never int casts.
  • NEW (#93) — npm run check:api diff-guards v1.d.ts against the LAST COMMIT — it fails mid-branch after a backend OpenAPI change until the regenerated file is committed; regen-idempotence (running generate:api twice → no diff) is the real sync check.
  • NEW (#93) — OSV advisories can break ALL CI overnight: NuGetAudit + warnings-as-errors turns a fresh critical advisory into NU1904 restore failures on every branch. Fix = a one-line central bump PR straight off main, merged before feature PRs (PR #137, Scriban 6→7 validated by both suites + unchanged XMLTV goldens).
  • NEW (#89) — Dialog/portal components: key open-effects on [open] ONLY and read callbacks through a latest-ref; an effect depending on an inline onClose re-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/browse mediaType is 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 generated IsSystem Playlist (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).
  • 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; #93: handoff said --status-warn-soft, the SPA token is --ctv-warn-soft — Toast precedent).
  • 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; 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; reduced-motion block lists a now-no-op .ctv-builder-libcard. From #93: enumeration endpoints missing for MPEG-TS scripts / audio language codes / UI cultures (settings fields are free-text meanwhile); media sources have no status/reachability signal in the API (Settings omits the StatusDot); full FFmpeg profile editor screen; edits made to an already-saved group DURING an in-flight save can be overwritten by the returned DTO (narrow race, noted by the verification fork). Filed: #126, #135.

PROMPT — #90: ChicoryTV rebrand (assets + naming)

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 (this issue is mostly mechanical — haiku/sonnet territory; fable only for the review fork). Read CLAUDE.md and the PROCESS + Lessons sections of this file first.

HARD CONSTRAINTS:

  • git worktree add .worktrees/issue-90-rebrand -b feat/90-rebrand origin/main; never touch the main checkout (it sits on docs/59-ui-redesign-brief). cd web && npm ci first.
  • Max 23 concurrent builds; ONE dotnet build at a time. Frontend-only expected — if backend strings turn out to be in scope, they're NOT (deeper product rebrand is epic #59).
  • NEVER set ETV_UPDATE_GOLDENS.
  • Review before PR: this is a small mechanical issue — a single fable correctness/design fork over the diff suffices (skip the 3-lens panel unless the diff grows); fixes via subagents. Merge needs an in-conversation consent question.

Task

Issue #90: apply the ChicoryTV rebrand across the SPA surface. Naming (ErsatzTV → ChicoryTV in user-facing UI copy), favicon/app icon/wordmark from design-system/assets/ (chicory-mark.svg, chicorytv-icon.svg, chicorytv-wordmark.svg), page title/meta. Acceptance: the SPA presents consistently as ChicoryTV. (Product-wide rebrand = epic #59.)

Approach notes

  1. Inventory first (delegate to Explore): every user-facing "ErsatzTV" in web/ (index.html title/meta/favicon, shell header, empty states, aria-labels, App.test.tsx copy assertions), what web/index.html currently ships as favicon, how the shell renders the brand mark today (the admin template uses chicory-mark.svg + "ChicoryTV" — mirror that), and whether vite needs assets in web/public/ vs imported.
  2. Do NOT rename API strings, C# namespaces, Docker images, or docs — SPA surface only. The legacy Blazor UI stays ErsatzTV.
  3. Favicon: derive from chicorytv-icon.svg (SVG favicon is fine for modern browsers; add a PNG fallback only if trivial).
  4. Tests: update copy assertions; baseline 145 web tests must stay green (some assert brand strings). dotnet suites should be untouched (frontend-only): ErsatzTV.Tests 495, Core.Tests 493 (+1 skip).
  5. PR → main: "feat(web): ChicoryTV rebrand (#90)", closes #90; poll CI by head SHA; ask "merge?"; verify main post-merge run.
  6. RELEASE CHECKPOINT: after #90 merges, the SPA is feature-complete-enough AND branded — this is the strongest v26.4.0 tag point before #91. Ask the user explicitly whether to tag v26.4.0 now (never [skip ci] the tagged commit).
  7. Update THIS handoff: pop #90, next = #91 cutover; record PR + main SHA + baselines. Commit to main. Print the next prompt in a fenced block.

Issue queue (work top-down)

  1. HOUSEKEEPING: #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 (note: Scriban already bumped to 7.2.5 by PR #137). MCP PR #76 (#58) still needs its rebase/refresh pass — good parallel track.
  2. #90 rebrand ← PROMPT above (small, mechanical; strongest v26.4.0 tag point at its merge).
  3. #91 cutover (+ tag v26.4.0 at the latest here — see RELEASE CHECKPOINT). Cross-refs: #66/#67 remain open image-pipeline nice-to-haves; #68 unblocked-independent. Done recently: PR #136 (#89 Channel Builder), PR #138 (#93 Settings — closed 2026-07-07, main 17f49304; web tests 112 → 145, ErsatzTV.Tests 447 → 495), PR #137 (Scriban GHSA CI-unblock), #92 design-sync round-trip closed (docs/design-sync.md).