Add a full ChannelEditScreen so the SPA can edit an existing channel
(the list/builder could only create). Mirrors the Blazor ChannelEditor
field set via hydrate-then-PUT against GET/PUT /api/channels/{id}.
- api/channels.ts: getChannelById + updateChannel (typed from generated
ChannelViewModel / UpdateChannelRequest).
- screens/ChannelEditScreen.tsx: section-rail form (General, Playout,
Streaming, Stream selection, Music video, Branding) following the
SettingsScreen draft/saved dirtiness + floating save bar pattern. Logo
upload reuses uploadArtwork; option data via existing pickers clients.
- App.tsx: /app/edit-channel/{id} route (allowSubPaths, screen parses the
id); repoint the channel-table edit pencil at it.
- Tests: API-client GET/PUT + screen hydrate/edit-save/404 coverage.
Deferred per scope: ExternalLogoUrl (backend contract gap), and the
/channels/numbers bulk renumber (existing Renumber toolbar = parity).
Language codes / stream selector / credits template are free-text inputs
(no enumeration endpoints exist); mirror-source list can't filter to
Generated-only (ChannelResponseModel omits playoutSource).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete the SPA-surface rebrand: SVG favicon (chicorytv-icon) served
from web/public, description + theme-color meta, sidebar brand mark
switched to the canonical transparent chicory-mark.svg per the design
system lockup, and the Channel Builder default group ErsatzTV->ChicoryTV
(tests updated). Legacy Blazor UI and backend strings intentionally
untouched (product-wide rebrand = epic #59).
closes#90
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SettingsScreen with linkable sub-sections (/app/settings/<section>),
settings API module + useSettingsScreenQuery, floating save bar that
PUTs only changed groups, custom resolution add/delete, legacy-UI
callouts for read-only resources. mockDashboardApi extended; 23 new
tests (suite 112 -> 134).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Normalize scan-status percent at the API boundary: the wire value is a
0-1 fraction despite the field name; the bar previously showed ~0% for
the whole scan (fixtures now use fractional wire values incl. 0/1)
- Fix the poll-start race: a triggered library enters a pending set that
survives status pruning until seen active or a 3-tick grace window
expires; the poll runs while pending ∪ active is nonempty (previously
the queue-to-start lag could mean polling never started and the button
re-enabled mid-scan); grace ticks also drain on persistent status-fetch
errors so the UI can't stick
- Keep setState updaters pure: pending/grace bookkeeping hoisted into
pruneGraceExpiredPending() outside the updater (StrictMode-safe)
- Wire the prototype's per-source Scan button (one POST per library,
disabled while any of its libraries is pending/active)
- Honest bare-500 media-sources failure test (backend has no exception
middleware); loadScanStatuses returns its promise; poll effect depends
on derived hasActiveScans; synchronous ref double-submit guard;
--radius-xs fallback 3px; StatusDot label on source cards (75 tests)
Review: PR #128 findings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Filler unit: treat fillerKind 'None'/null as non-filler (real items no
longer render the Filler badge); add the prototype's Show filler toggle
wired to ?showFiller=true (the client previously never requested filler,
making the filler UI dead code); fixtures now use real wire values
- Hoist selection fetch out of the setState updater (StrictMode-safe)
via a baseRef snapshot; merge polled channel states functionally
instead of clobbering with the captured snapshot
- Fix undefined --ctv-surface-1 token on the hero (→ --surface-card per
prototype); correct --text-lg fallback; collapse the new playout grids
in the 980px media block (no page-level horizontal scroll)
- Visible deferrals: disabled Reset / Schedule reset buttons with note
(no per-playout reset endpoint yet)
- itemsLoading spinners on selection switch; time-window now-matching
with title fallback; rail disabled while reset-all mutates
- Tests: mount-count delta isolation from the Dashboard prefetch;
reset-all reframed as transport-failure (mock honors fixture status;
backend is unconditionally 202); rail filter test; paging honesty
('shown of total' label); listPlayout() fixture matches the list DTO
(63 tests)
Review: PR #127 findings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add a schedule selector (previously only the first schedule was ever
shown); switching loads items only, with a pane-level spinner
- Fix duplicated mount effect that fetched the whole screen twice
- Add a collection picker to the add-item flow (was silently using the
alphabetically-first collection with no way to correct it)
- Reorder now reconciles from the PUT response; add/delete refetch items
only — pickers are no longer refetched on every mutation
- Guard reorder against double-submit via a ref; remove the inert Save
button; document the widened item type (OpenAPI gap → #126)
- Inspector fidelity: Tail filler row + Keep multi-part together
- a11y: aria-current for the active item, draggable role description
- CSS: correct --text-md fallbacks; replace nonexistent --text-faint
with --text-disabled in new blocks
- Tests: single-fetch-on-mount assertion, chosen-collection POST body,
picker-failure path, collection() fixture with all required fields;
drop unreachable mock branch (52 tests)
Review: PR #125 findings; refs #126
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Derive selectedChannels from visibleChannels and clear selection on
filter change so bulk operations can never act on rows hidden by the
active filter (reachable via the 30s state poll under the on-air filter)
- Disable the per-row delete button while a mutation is in flight
- Correct the --pad-cell-x fallback (token resolves to 16px)
- Add tests: bulk renumber request body + selection clearing, and
selection reset across filter changes
Review: PR #124 findings 1, 2, 4, 8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Match the backend's exact health status contract ('pass'|'fail'|'warn'|'info')
instead of fictional strings; 'info' now renders as a neutral/idle presentation
and is excluded from failing/warning counts in summarizeHealth.
- Drop the redundant /api/version fetch from getDashboardData/DashboardData;
SidebarVersion's useDashboardVersionQuery remains the single source.
- Disable "Refresh health" (via Button's loading prop) while a health request
is in flight to prevent concurrent double-click requests.
- Add the active-flag unmount guard to useDashboardHealthQuery for consistency
with useDashboardQuery/useChannelsQuery.
- Remove dead .ctv-activity-*/.ctv-release-* CSS left over from the removed
activity feed/release notes UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>