feat(web): Channel Builder (#89) #136

Merged
timothy merged 4 commits from feat/89-channel-builder into main 2026-07-06 23:40:44 +02:00
Owner

closes #89

The flagship library-to-lineup Channel Builder — one screen replacing the Channel → Schedule → Items → Playout editor chain, recreated from design-system/design_handoff_channel_builder/ at high fidelity.

What's in it

  • web/src/builder/ChannelBuilder.tsx (~1,300 lines, own module — App.tsx was already 2,890 lines): 3-column layout (minmax(0,1.2fr) minmax(0,1fr) 304px, independent scroll, 980px collapse). Library browser (Library/Collections segmented, debounced search, per-library filter chips, poster grid/compact density, Load more paging; Collections mode = 5 typed browse calls merged client-side). Native HTML5 drag-and-drop lineup (drag from library or double-click to add, drag-to-reorder, dedupe). Settings rail: name, AUTO channel number (computed client-side from GET /api/channels, validated + uniqueness-checked), channel image dropzone with guide-logo/bug previews (upload deferred to Create; separate bug image visibly deferred), Shuffle → advanced.playbackOrder, Always-playing → advanced.playoutMode (live-styled row), Channel Template picker with save-as-template, Advanced override/view-defaults accordion over the real request surface.
  • First Dialog/ConfirmDialog primitive (components/overlay.tsx) — portal, Esc/backdrop close, scroll lock, aria wiring, reduced motion.
  • API modules: libraryBrowse, channelTemplates (default 404→null), artwork (multipart /api/artwork/uploads), pickers (client-sorted lists), createChannelFromLineup; schedules.ts now sources filler/watermark fetchers from pickers.ts (barrel dedupe).
  • Contract semantics surfaced at add time: RerunCollection/Playlist rows error the moment the lineup goes multi-item; MultiCollection + non-shuffle order errors until the Shuffle toggle clears it; server 422s naming lineup[i] map back to row highlights (cleared on lineup mutation). Advanced pickers offer "Inherit from template" instead of a fake "None" — the handler coalesces null with the template value, so clear-to-none is not expressible (filed #135).
  • group defaults to "ErsatzTV" (required by the handler; editable under Advanced → Behavior). Successful create navigates to /app/channels; artwork uploads are cached across create retries.

Review

Three parallel lenses (fable correctness fork + contract/tests + design-fidelity) → 1 blocker (Dialog focus-steal) + 4 substantial (Inherit semantics, toggle label double-fire, Advanced scroll-into-view, .ctv-lift/.ctv-press motion port) + fixture-fidelity minors, all fixed in 54a0760f; fork verification over the fix diff: SHIP.

Verification

npm run typecheck / npm run test (112 passed, baseline 80 → +32) / npm run build all green. All 3 themes covered by tests.

🤖 Generated with Claude Code

closes #89 The flagship library-to-lineup **Channel Builder** — one screen replacing the Channel → Schedule → Items → Playout editor chain, recreated from `design-system/design_handoff_channel_builder/` at high fidelity. ## What's in it - **`web/src/builder/ChannelBuilder.tsx`** (~1,300 lines, own module — App.tsx was already 2,890 lines): 3-column layout (`minmax(0,1.2fr) minmax(0,1fr) 304px`, independent scroll, 980px collapse). Library browser (Library/Collections segmented, debounced search, per-library filter chips, poster grid/compact density, Load more paging; Collections mode = 5 typed browse calls merged client-side). Native HTML5 drag-and-drop lineup (drag from library or double-click to add, drag-to-reorder, dedupe). Settings rail: name, AUTO channel number (computed client-side from GET /api/channels, validated + uniqueness-checked), channel image dropzone with guide-logo/bug previews (upload deferred to Create; separate bug image visibly deferred), Shuffle → `advanced.playbackOrder`, Always-playing → `advanced.playoutMode` (live-styled row), Channel Template picker with save-as-template, Advanced override/view-defaults accordion over the real request surface. - **First `Dialog`/`ConfirmDialog` primitive** (`components/overlay.tsx`) — portal, Esc/backdrop close, scroll lock, aria wiring, reduced motion. - **API modules**: `libraryBrowse`, `channelTemplates` (default 404→null), `artwork` (multipart `/api/artwork/uploads`), `pickers` (client-sorted lists), `createChannelFromLineup`; `schedules.ts` now sources filler/watermark fetchers from `pickers.ts` (barrel dedupe). - **Contract semantics surfaced at add time**: RerunCollection/Playlist rows error the moment the lineup goes multi-item; MultiCollection + non-shuffle order errors until the Shuffle toggle clears it; server 422s naming `lineup[i]` map back to row highlights (cleared on lineup mutation). Advanced pickers offer **"Inherit from template"** instead of a fake "None" — the handler coalesces null with the template value, so clear-to-none is not expressible (filed **#135**). - `group` defaults to `"ErsatzTV"` (required by the handler; editable under Advanced → Behavior). Successful create navigates to /app/channels; artwork uploads are cached across create retries. ## Review Three parallel lenses (fable correctness fork + contract/tests + design-fidelity) → 1 blocker (Dialog focus-steal) + 4 substantial (Inherit semantics, toggle label double-fire, Advanced scroll-into-view, `.ctv-lift`/`.ctv-press` motion port) + fixture-fidelity minors, all fixed in 54a0760f; fork verification over the fix diff: SHIP. ## Verification `npm run typecheck` / `npm run test` (**112 passed**, baseline 80 → +32) / `npm run build` all green. All 3 themes covered by tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 4 commits 2026-07-06 23:34:43 +02:00
- components/overlay.tsx: first Dialog/ConfirmDialog (portal, esc/backdrop
  close, scroll lock, aria wiring, reduced-motion aware) + 7 tests
- api: libraryBrowse, channelTemplates (default 404->null), artwork upload
  (multipart /api/artwork/uploads), pickers (client-sorted lists incl.
  /api/ffmpeg/profiles), createChannelFromLineup in channels.ts
- schedules.ts now sources filler/watermark fetchers from pickers.ts
  (avoids duplicate barrel exports)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3-column library-to-lineup builder replacing the four-editor chain:
library browser (search/filter/poster grid, double-click or drag to add,
collections via 5 typed browse calls) · drag-to-reorder lineup with
add-time constraint surfacing (Rerun/Playlist only-valid-alone,
MultiCollection requires shuffle, server lineup[i] 422 mapping) ·
settings rail (AUTO number, channel image w/ guide-logo + bug preview,
shuffle/always-playing toggles with overrides-template tags, template
picker + save-as-template, advanced override/view-defaults accordion).
Creates channel atomically via POST /api/channels/from-lineup, artwork
uploaded at create time. navigateToPath extracted to routing.ts to
avoid an App<->builder import cycle. 16 new tests (106 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dialog: focus only on open transition (was stealing focus from its own
  inputs every parent re-render); Escape listener via latest-ref
- Advanced overrides: honest 'Inherit from template' semantics - the
  handler coalesces null with the template value so 'None' could never
  clear anything (clear-to-none follow-up: #135); save-as-template now
  composes from the same effective-value helper
- toggle rows: label->div (label activation forwarding double-fired in
  real browsers); clear stale 422 row highlight on lineup mutations
- port .ctv-lift/.ctv-press motion utilities + apply per prototype;
  Advanced expand scroll-into-view; Info glyph on advanced info line
- fixture fidelity: real 'Validation failed' title, verbatim handler
  detail message, omit null keys, collectionKind on rerun fixture
- cache successful artwork upload across create retries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test(web): Channel Builder coverage — collections fan-out, paging, save-as-template, 413, zero-templates, lineup item shape (refs #89)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
7d37e554d3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy merged commit e1860785cc into main 2026-07-06 23:40:44 +02:00
timothy deleted branch feat/89-channel-builder 2026-07-06 23:40:45 +02:00
Sign in to join this conversation.