Iterated Auto-Tune prototype pulled back from the Claude Design working copy (project 4eb64b16) into design-system/. The 3-step wizard (Configure → Preview → Create) is PR2; the per-channel DetailPanel it also contains is deferred to the #383 arc (#384/#385/#386) per design-first — new endpoints backlogged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Handoff: ChicoryTV — Auto-Tune (generate channels from library metadata, #69)
Overview
Auto-Tune is the automatic-first channel-creation mode — the counterpart to the manual Channel Builder ("New Channel"). Instead of building one channel by hand, it enumerates your library's metadata along one or more axes (TV Shows, TV Genres, Movie Genres), previews the whole set of proposed channels so you can pick which to keep, then bulk-creates the selected ones. It is additive and non-destructive: it never edits or deletes an existing channel; name and number collisions are skipped, never overwritten. Each generated channel is backed by a live SmartCollection query, so a "Comedy" channel keeps picking up new comedies as the library grows.
Concept borrowed from PseudoTV Live's signature Auto-Tuning, deliberately fixing its two weaknesses: PseudoTV is all-or-nothing per category with no preview, and it wipes+rebuilds the whole lineup on every run. Ours adds a preview/select step and is non-destructive.
The backend already shipped (PR1, ersatztv#379) — two endpoints under the frozen /api/v1. This
handoff is PR2: the SPA screen that drives them.
Scope note (2026-07-16). The prototype (
AutoTune.jsx) was iterated to add a per-channel DetailPanel slide-over (opened from a "Configure" button on each Preview row — full per-channel editor: identity/image, shuffle/always-playing, template + Advanced overrides, weighted content sources, exclude/add-untagged, example schedule). That panel needs backend the PR1 endpoints don't have, so it is DEFERRED to a follow-up arc — ersatztv#383 (children #384 enumerate members, #385 per-channel overrides + rotation weights in create, #386 the SPA panel). PR2 (this handoff) implements the 3-step wizard only — Configure → Preview → Create — on the existing PR1 endpoints; no per-row Configure button. The DetailPanel sections below the wizard spec are recorded for the #383 arc, not PR2.
About the design files
The files in this bundle are design references built in HTML/React (a prototype on the ChicoryTV
UI kit) — not production code. Recreate the design in the target codebase (the ChicoryTV React
SPA, web/) using its real components (web/src/components/) and CSS-custom-property tokens. The
prototype (AutoTune.jsx) is the source of truth for layout, spacing, motion, and interaction;
its mock data and mock outcome logic are illustrative only — the real screen calls the API.
Fidelity
High-fidelity. Colors, typography, spacing, radii, and interactions are resolved and use tokens throughout, so it is fully theme-aware (verified in the warm + cool accent themes). All values below are exact.
The screen — a 3-step wizard
One full-height column: a fixed toolbar (12px 20px padding, 1px solid var(--border-hairline)
bottom border) over a scrolling <main>. The toolbar is constant across steps; its content and
primary action change per step.
Toolbar (all steps):
- Left: a 34×34 accent icon tile (
var(--radius-sm),background: var(--ctv-accent-soft),color: var(--ctv-accent)) with theSparklesicon; then title "Auto-Tune" (--text-md, semibold) + subtitle "Generate channels from your library" (--text-xs,--text-secondary). - Center: a step rail — three steps (
Configure · Preview · Create) joined by 26px hairline connectors. Each step is a 20px round chip + label. The active step: chipbackground var(--ctv-accent)/color var(--text-on-accent), label accent + semibold. A completed step: chipvar(--ctv-accent-soft)bg + accentCheckicon, label--text-secondary. A future step: chipvar(--ctv-surface-2)+--text-disabled. - Right: the contextual primary action (below).
Step 1 — Configure
Centered column, max-width: 860px, padding: 26px 24px 40px.
- Intro paragraph (
--text-sm,--text-secondary,max-width: 620px): what Auto-Tune does + the non-destructive promise. - "Generate from" section (eyebrow label) — a 3-column grid of selectable axis cards
(one per axis). Each card is a
<button>(ctv-pressfor the tactile scale-on-press):- Selected:
1px solid var(--ctv-accent)border,var(--ctv-accent-soft)bg,var(--shadow-sm); a filled accentCheckCircle2top-right; the icon tile isvar(--ctv-accent)/var(--text-on-accent). - Unselected:
1px solid var(--border-control),var(--surface-card)bg; aCircleoutline top-right (--text-disabled); icon tilevar(--ctv-surface-2)/--text-secondary. - Content: axis icon (Tv / Clapperboard / Film), title, one-line tagline, and a bottom
order chip (
ListOrdered"Episode order" for TV Shows;Shuffle"Shuffled" for the genres).
- Selected:
- "Defaults" section — a 2-column grid of four labelled fields, each with a helper line
(
Fieldcomponent: label--text-xsmedium, hint--text-2xs--text-disabled):- Minimum items (
Input type=number,Hashleading icon) — skip channels below this count. - Starting channel number (
Input type=number,Tvicon) — numbers count up from here. - Channel group (
Input,FolderTreeicon) — the group every generated channel lands in. - Channel template (
Select) — the batch's streaming/playout/filler defaults.
- Minimum items (
Primary action: Preview channels (primary, Eye icon). Disabled with a tooltip until ≥1
axis is selected.
Step 2 — Preview
Centered column, max-width: 920px.
- Summary bar:
N of M selected(mono N), an info Tag"{k} already exist — deselected"when any proposal's name collides, a spacer, then ghost Select all / Clear buttons (they only touch selectable — non-existing — rows). - One section per axis that produced rows (
var(--surface-card), hairline border,var(--radius-md)), in axis order (TV Shows → TV Genres → Movie Genres). Section header (var(--ctv-bg-sunken)): a group checkbox (tri-state: checked / indeterminate when partial), the axis icon + title, a neutral Badge with the row count, and a right-aligned order eyebrow. - Rows (hairline-separated): per-row Checkbox, the allocated number (mono,
min-width 42), the channel name (semibold, ellipsis) over afrom "{value}"sub-line, a neutral Badge{itemCount} items, and — for an already-existing name — anExistsTag. Existing rows render atopacity: 0.55with a disabled, unchecked checkbox (dedup: you can't re-create a channel that already exists by that name). - Empty state (no proposals matched): centered
SearchX+ "try lowering the minimum items".
Primary action: Create {N} channels (primary, Check icon; label pluralizes; disabled until
≥1 selected) preceded by a ghost Back (ArrowLeft) that returns to Configure.
Step 3 — Create (results)
Centered column, max-width: 860px.
- Three
Stattiles across the top: Created / Skipped / Failed counts (iconsCheckCircle2/MinusCircle/XCircle). - A "Added to group {group}" line (
FolderTreeicon). - A results list (card, hairline rows): per channel a status glyph in the status color
(
CheckCircle2live-green /MinusCirclesecondary /XCircledanger), the name, an optional reason sub-line (skip/fail explanation), the new#{channelId}(mono) when created, and a status Badge (positive/neutral/danger).
Primary action: Start over (primary, RotateCcw) — resets the wizard to Configure.
API mapping (the real screen)
Both endpoints already exist (PR1). The client sends only axis + value back — never a Lucene
query; the server regenerates it (query authorship is server-side only).
| UI element | Endpoint / field |
|---|---|
Preview channels |
POST /api/v1/channels/auto-tune/preview |
| — axis cards | request axes: AutoTuneAxis[] ("TvShow" | "TvGenre" | "MovieGenre") |
| — Minimum items | request minItems: number |
| — Starting channel number | request startingNumber: number |
| Preview rows | response AutoTuneProposalResponseModel[]: { axis, value, name, number (string), itemCount, alreadyExists } |
| — number chip | number (string — channel numbers can be "500.1"; render as-is) |
— {itemCount} items badge |
itemCount |
— greyed + Exists |
alreadyExists === true (deselect + disable) |
Create {N} channels |
POST /api/v1/channels/auto-tune |
| — Channel template field | request templateId: number (from GET /api/v1/channel-templates / …/default) |
| — Channel group field | request group: string |
| — selected rows | request channels: { axis, value, name, number }[] (echo the selected proposals) |
| Results tiles + list | response AutoTuneResultResponseModel: { results: { name, status ("Created"|"Skipped"|"Failed"), channelId, reason }[], createdCount, skippedCount, failedCount } |
Notes:
- Numbers are advisory. The preview allocates them (skipping taken numbers); the create handler
re-validates at create time — a number taken in between yields a per-channel
Skipped, not a batch failure. So aSkippedoutcome with "number … already taken" is normal, not an error. - Default selection = every proposal whose
alreadyExistsis false. - Grouping/ordering is fixed: axis order (TvShow, TvGenre, MovieGenre) then by value; mirror the server's ordering rather than re-sorting client-side.
- Template default: preselect
GET /api/v1/channel-templates/default(fall back to the first template) so the field is never empty; the batch requires atemplateId.
Real-SPA implementation notes (target = web/)
- New screen
web/src/screens/AutoTuneScreen.tsx; new route idautoTune, path/app/auto-tune, label "Auto-Tune", iconSparkles, placed right afterbuilderin the first sidebar nav group (the two channel-creation modes sit together). NoprimaryActionlabel — the wizard's action lives in-body (perspa-conventions.md§10: the "+" banner is for single unambiguous create list screens; a multi-step wizard drives its own buttons), so the shell shows no banner button for this screen. - New API module
web/src/api/autoTune.ts(re-export the generated DTOs;previewAutoTune(body)+createAutoTunedChannels(body)over the sharedrequesthelper; amessageFrom…Errornarrower), re-exported fromweb/src/api/index.ts. - Reuse
getChannelTemplates/getDefaultChannelTemplatefromweb/src/api/channelTemplates.tsfor the templateSelect. - Follow
spa-conventions.md§3 for the two async calls (discriminated-union state,seqRef+activeRef, no synchronous set-state in an effect body). The wizard is transient (a create flow, not an editor of persisted data), so it does not register the §8 unsaved-changes guard. - Map prototype primitives → real components:
Button,Input,Select,Checkbox,Badge,Tag,Stat,Tooltip,Spinnerfromweb/src/components/. The axis cards and step rail are small screen-local components built fromctv-*utility classes + tokens (no new shared primitive). - Docs to update in the same PR:
docs/domain-model.md(add/app/auto-tuneto the channel routes),docs/blazor-route-parity.md(net-new SPA screen, no Blazor ancestor),docs/spa-conventions.md(if the wizard/step-rail pattern is worth recording), anddesign-system/committed alongside.