ChicoryTV SPA: Channel Builder (primary deliverable) #89
Notifications
Due Date
No due date set.
Blocks
Depends on
#91 ChicoryTV SPA: Blazor → SPA cutover (retire old UI)
timothy/ersatztv
#63 Composite "create channel from lineup" endpoint
timothy/ersatztv
#82 ChicoryTV SPA: app shell (sidebar + top bar + routing)
timothy/ersatztv
#104 Backend: logo/watermark image upload endpoint
timothy/ersatztv
Reference: timothy/ersatztv#89
Reference in New Issue
Block a user
Recreate the library-to-lineup Channel Builder — the flagship screen that replaces the Channel → Schedule → Items → Playout editor chain. Prototype:
design-system/templates/chicorytv-admin/ChannelBuilder.jsx; full spec:design-system/design_handoff_channel_builder/README.mdanddocs/superpowers/specs/2026-07-01-chicorytv-ui-redesign-design.md§6.3-column: library browser (search/filter/poster grid, drag + double-click to add) · reorderable lineup · settings rail (name/number, channel image → guide logo + on-screen bug, shuffle, always-playing, Channel Template picker, Advanced override).
Blocked by backend epic #62 — needs the composite create endpoint (#63), Channel Templates (#64), and library browse/search API with artwork (#65). Build against those as they land.
Acceptance: creates a working channel atomically from a lineup; matches prototype; all 3 themes.
Audit update (2026-07-02)
Prototype-vs-API diff results (plan audit under #59):
ChannelPlayoutMode.Continuous/OnDemandfield. #68 (resume/bookmark) is an independent playout feature.Approach (orchestrator plan, 2026-07-06)
Branch
feat/89-channel-builderoff main (a126970dhead, post-#63). Frontend-only; all backend deps verified on main:GET /api/library/browse,GET /api/channel-templates[/default],POST /api/artwork/uploads(note: actual path has/uploads; multipartfile+target=logo|watermark),POST /api/channels/from-lineup, pickers/api/filler-presets,/api/watermarks,/api/graphics-elements,/api/ffmpeg/profiles.Component breakdown
web/src/components/overlay.tsx— newDialogprimitive (+ConfirmDialog) + CSS in components.css. First modal in the SPA (known backlog gap). Used for: cancel-with-dirty-lineup confirm, clear-lineup confirm, "Save current settings as template…" (name/description form →POST /api/channel-templates).api/libraryBrowse.ts,api/channelTemplates.ts(default via/default, 404-tolerant),api/artwork.ts(FormData upload),api/pickers.ts(filler/watermark/graphics/ffmpeg-profile lists, client-sorted — DB order on the wire),createChannelFromLineupinapi/channels.ts.web/src/builder/ChannelBuilder.tsx— the screen, in its OWN module (deviation from the everything-in-App.tsx pattern, stated reason: App.tsx is already 2890 lines and this is the largest screen yet). Wired into the existingbuilderroute (/app/new-channel, currently a placeholder) viaScreenContent.minmax(0,1.2fr) minmax(0,1fr) 304px+ the established 980px collapse block. Tests extendmockDashboardApiin App.test.tsx (established fixture pattern).State shape (per prototype README, native HTML5 DnD like the prototype — no new dep)
Library pane:
source(library|collections), debouncedquery,libraryFilter(chips from GET /api/libraries sources),compact, paged browse (pageSize 100 + load-more). Collections mode = 5 parallel typed browse calls (Collection/Smart/Multi/Rerun/Playlist) merged client-side, sincemediaTypeis a single-valued param. Lineup: orderedLibraryBrowseItemResponseModel[]dedup'd by (mediaType,id); drag statedragLib/dragRow/overRow. Rail:name,number(AUTO = max integer part over GET /api/channels + 1; validated against^[0-9]+(\.[0-9]{1,2})?$+ uniqueness), logo file (client-side type/size check <30 MB — server 413 is bare; upload deferred to Create so Cancel orphans nothing),shuffle,always,templateId, advancedmode(closed|view|override) + touched-field overrides.Semantics mapping (from #63's contract)
advanced.playbackOrder=Shuffle|Chronological(always sent explicitly — handler default is Chronological regardless of template). Toggle default on template select comes fromtemplate.shuffleScheduleItems(the template's declared shuffle intent; templates carry no playbackOrder), "overrides template" tag when diverging.advanced.playoutMode=Continuous|OnDemand; default fromtemplate.playoutMode.groupis required by the handler → default"ErsatzTV"(Blazor editor precedent), editable under Advanced/Behavior.lineup[i]map back to row highlights as the backstop.from-lineup→ navigate to/app/channels. No languages endpoint exists — preferred audio/subtitle language stay free-text code inputs (graceful; no follow-up needed).Subagents
Dialog primitive (sonnet) ∥ API modules (sonnet) → screen + wiring + tests (opus, judgment-heavy DnD/state) → verify (typecheck/test/build, baseline 80 web tests) → review lenses (fable correctness fork + sonnet contract/tests + design-system lens vs prototype) → fixes via subagents + fork verification → PR.
Progress: foundation landed on
feat/89-channel-builder(8e113623) — firstDialog/ConfirmDialogprimitive incomponents/overlay.tsx(portal, Esc/backdrop close, scroll lock, aria, reduced-motion; 7 tests) + all API modules (libraryBrowse,channelTemplateswith 404→null default,artworkmultipart upload,pickersclient-sorted lists incl./api/ffmpeg/profiles,createChannelFromLineup). One deviation:schedules.ts's duplicate filler/watermark fetchers moved intopickers.ts(the barrelexport *would otherwise produce ambiguous re-exports). Suite 90/90 green, typecheck clean.Screen implementation (3-column builder, DnD lineup, settings rail, template picker, advanced overrides, create flow +
lineup[i]422 mapping) now in progress via an opus subagent.Two contract notes discovered while planning, for the record: the upload path is
POST /api/artwork/uploads(the #89 audit text says/api/artwork), and the create handler requires a non-emptygroup(defaulting to"ErsatzTV"per Blazor editor precedent, editable under Advanced → Behavior).Closed via PR #136 (main
e1860785)What was done: the full 3-column Channel Builder at
/app/new-channel— library browser (Library/Collections segmented, debounced search, library filter chips, poster/compact density, Load more; Collections = 5 typed browse calls merged), native-DnD reorderable lineup with add-time constraint surfacing (Rerun/Playlist only-valid-alone, MultiCollection requires shuffle, serverlineup[i]422 → row highlight), settings rail (AUTO number computed client-side, image dropzone + guide-logo/bug previews, Shuffle→playbackOrder + Always-playing→playoutMode toggles with overrides-template tags, template picker + save-as-template, Advanced override/view-defaults accordion). Creates the channel atomically viaPOST /api/channels/from-lineup(artwork uploaded at create time, cached across retries) and navigates to Channels. All 3 themes tested. Also ships the SPA's firstDialog/ConfirmDialogprimitive and API modules for browse/templates/artwork/pickers.Files changed (17, +4272/−22):
web/src/builder/ChannelBuilder.tsx(new),web/src/components/overlay.tsx(new) + components.css/index,web/src/api/{libraryBrowse,channelTemplates,artwork,pickers}.ts(new) + channels/schedules/index,web/src/routing.ts(new, breaks an App↔builder import cycle),App.tsx(route wiring),shell.css(.ctv-builder-* + .ctv-lift/.ctv-press utilities + 980px collapse),App.test.tsx(+22 tests; suite 80 → 112).Review: 3 parallel lenses (fable correctness fork, contract/tests, design fidelity) → 1 blocker (Dialog focus-steal on re-render) + 4 substantial (advanced "None" overrides silently inheriting the template value, toggle-row label double-fire in real browsers, missing Advanced scroll-into-view, missing lift/press motion language) — all fixed (
54a0760f) and fork-verified.Deferred / follow-ups:
??-coalesces null → inherit); the UI ships honest "Inherit from template" pickers until then.target=watermarkupload when #67-style separation is wanted.Contract corrections vs the audit text: upload path is
POST /api/artwork/uploads; create requires non-emptygroup(defaults to "ErsatzTV", editable under Advanced → Behavior); AUTO number is client-computed (no server suggestion, per #63's close-out).Docs: handoff
docs/handoffs/chicorytv-issue-queue.mdupdated (next: #93 Settings).