ChicoryTV SPA: Channel Builder (primary deliverable) #89

Closed
opened 2026-07-02 01:07:19 +02:00 by timothy · 3 comments
Owner

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.md and docs/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):

  • New hard blocker: #104 (logo/watermark image upload) — no upload endpoint exists anywhere; the image dropzone has nowhere to POST. #66/#67 assumed it; neither scoped it.
  • New soft blocker: #105 (filler-preset / watermark / graphics-element list endpoints) — the Advanced rail pickers have no data source.
  • #65 scope amended to include item duration/counts, collection metadata, library-list chips, and typed picker resolution.
  • #68 is NOT a prerequisite for this screen: the "Always playing" toggle is already served by the existing writable ChannelPlayoutMode.Continuous/OnDemand field. #68 (resume/bookmark) is an independent playout feature.
  • #66 is non-blocking (nice-to-have format support).
  • Minor: fold a "next free channel number" suggestion (the AUTO badge) into #63's composite-create response.
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.md` and `docs/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): - **New hard blocker: #104** (logo/watermark image upload) — no upload endpoint exists anywhere; the image dropzone has nowhere to POST. #66/#67 assumed it; neither scoped it. - **New soft blocker: #105** (filler-preset / watermark / graphics-element list endpoints) — the Advanced rail pickers have no data source. - **#65 scope amended** to include item duration/counts, collection metadata, library-list chips, and typed picker resolution. - **#68 is NOT a prerequisite** for this screen: the "Always playing" toggle is already served by the existing writable `ChannelPlayoutMode.Continuous/OnDemand` field. #68 (resume/bookmark) is an independent playout feature. - **#66 is non-blocking** (nice-to-have format support). - Minor: fold a "next free channel number" suggestion (the AUTO badge) into #63's composite-create response.
timothy added the frontend label 2026-07-02 01:07:19 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:43 +02:00
timothy added a new dependency 2026-07-02 19:44:16 +02:00
Author
Owner

Approach (orchestrator plan, 2026-07-06)

Branch feat/89-channel-builder off main (a126970d head, 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; multipart file + 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 — new Dialog primitive (+ 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 modules: 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), createChannelFromLineup in api/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 existing builder route (/app/new-channel, currently a placeholder) via ScreenContent.
  • shell.css: builder 3-col grid minmax(0,1.2fr) minmax(0,1fr) 304px + the established 980px collapse block. Tests extend mockDashboardApi in 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), debounced query, 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, since mediaType is a single-valued param. Lineup: ordered LibraryBrowseItemResponseModel[] dedup'd by (mediaType,id); drag state dragLib/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, advanced mode (closed|view|override) + touched-field overrides.

Semantics mapping (from #63's contract)

  • Shuffle toggle → advanced.playbackOrder = Shuffle|Chronological (always sent explicitly — handler default is Chronological regardless of template). Toggle default on template select comes from template.shuffleScheduleItems (the template's declared shuffle intent; templates carry no playbackOrder), "overrides template" tag when diverging.
  • Always playing → advanced.playoutMode = Continuous|OnDemand; default from template.playoutMode.
  • group is required by the handler → default "ErsatzTV" (Blazor editor precedent), editable under Advanced/Behavior.
  • Add-time constraint surfacing (not submit-time): RerunCollection/Playlist rows get an error badge the moment the lineup goes multi-item (422 otherwise); MultiCollection + Sequential gets a "requires Shuffle" row error. Create is disabled with a tooltip naming the offending row. Server 422s naming lineup[i] map back to row highlights as the backstop.
  • Create flow: ref-guarded; upload artwork first (if set) → 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.

## Approach (orchestrator plan, 2026-07-06) Branch `feat/89-channel-builder` off main (a126970d head, 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`; multipart `file` + `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` — new `Dialog` primitive** (+ `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 modules**: `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), `createChannelFromLineup` in `api/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 existing `builder` route (`/app/new-channel`, currently a placeholder) via `ScreenContent`. - shell.css: builder 3-col grid `minmax(0,1.2fr) minmax(0,1fr) 304px` + the established 980px collapse block. Tests extend `mockDashboardApi` in 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), debounced `query`, `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, since `mediaType` is a single-valued param. Lineup: ordered `LibraryBrowseItemResponseModel[]` dedup'd by (mediaType,id); drag state `dragLib/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`, advanced `mode` (closed|view|override) + touched-field overrides. ### Semantics mapping (from #63's contract) - **Shuffle toggle** → `advanced.playbackOrder` = `Shuffle`|`Chronological` (always sent explicitly — handler default is Chronological regardless of template). Toggle *default* on template select comes from `template.shuffleScheduleItems` (the template's declared shuffle intent; templates carry no playbackOrder), "overrides template" tag when diverging. - **Always playing** → `advanced.playoutMode` = `Continuous`|`OnDemand`; default from `template.playoutMode`. - **`group`** is required by the handler → default `"ErsatzTV"` (Blazor editor precedent), editable under Advanced/Behavior. - **Add-time constraint surfacing** (not submit-time): RerunCollection/Playlist rows get an error badge the moment the lineup goes multi-item (422 otherwise); MultiCollection + Sequential gets a "requires Shuffle" row error. Create is disabled with a tooltip naming the offending row. Server 422s naming `lineup[i]` map back to row highlights as the backstop. - **Create flow**: ref-guarded; upload artwork first (if set) → `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.
Author
Owner

Progress: foundation landed on feat/89-channel-builder (8e113623) — first Dialog/ConfirmDialog primitive in components/overlay.tsx (portal, Esc/backdrop close, scroll lock, aria, reduced-motion; 7 tests) + all API modules (libraryBrowse, channelTemplates with 404→null default, artwork multipart upload, pickers client-sorted lists incl. /api/ffmpeg/profiles, createChannelFromLineup). One deviation: schedules.ts's duplicate filler/watermark fetchers moved into pickers.ts (the barrel export * 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-empty group (defaulting to "ErsatzTV" per Blazor editor precedent, editable under Advanced → Behavior).

**Progress**: foundation landed on `feat/89-channel-builder` (8e113623) — first `Dialog`/`ConfirmDialog` primitive in `components/overlay.tsx` (portal, Esc/backdrop close, scroll lock, aria, reduced-motion; 7 tests) + all API modules (`libraryBrowse`, `channelTemplates` with 404→null default, `artwork` multipart upload, `pickers` client-sorted lists incl. `/api/ffmpeg/profiles`, `createChannelFromLineup`). One deviation: `schedules.ts`'s duplicate filler/watermark fetchers moved into `pickers.ts` (the barrel `export *` 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-empty `group` (defaulting to `"ErsatzTV"` per Blazor editor precedent, editable under Advanced → Behavior).
Author
Owner

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, server lineup[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 via POST /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 first Dialog/ConfirmDialog primitive 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:

  • #135 (new): from-lineup advanced overrides can't express clear-to-none (handler ??-coalesces null → inherit); the UI ships honest "Inherit from template" pickers until then.
  • Separate bug (watermark) image is a visibly disabled affordance — one uploaded image drives logo+bug; wire target=watermark upload when #67-style separation is wanted.
  • No languages list endpoint exists — preferred audio/subtitle languages are free-text code inputs (graceful; no follow-up filed).
  • Prototype's custom overlay-scrollbar ScrollArea replaced by native thin scrollbars (documented deviation).

Contract corrections vs the audit text: upload path is POST /api/artwork/uploads; create requires non-empty group (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.md updated (next: #93 Settings).

## 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, server `lineup[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 via `POST /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 first `Dialog`/`ConfirmDialog` primitive 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**: - **#135** (new): from-lineup advanced overrides can't express clear-to-none (handler `??`-coalesces null → inherit); the UI ships honest "Inherit from template" pickers until then. - Separate bug (watermark) image is a visibly disabled affordance — one uploaded image drives logo+bug; wire `target=watermark` upload when #67-style separation is wanted. - No languages list endpoint exists — preferred audio/subtitle languages are free-text code inputs (graceful; no follow-up filed). - Prototype's custom overlay-scrollbar ScrollArea replaced by native thin scrollbars (documented deviation). **Contract corrections vs the audit text**: upload path is `POST /api/artwork/uploads`; create requires non-empty `group` (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.md` updated (next: #93 Settings).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: timothy/ersatztv#89