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>
Reference data (profiles, watermarks, fillers, media sources, version,
health) now loads via allSettled with per-resource failure notes; only
the settings groups themselves gate the screen. Error state renders
before the loading guard (was unreachable). +2 tests (136 total).
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>
- 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>
Adversarial review found the previous multi-flood design non-viable:
PlayoutModeSchedulerFlood never yields to a following Dynamic-start
schedule item, so only the first item ever played, and grouping
media items into a Collection silently dropped the requested order.
Redesign:
- Single-item lineup: one ProgramScheduleItemFlood referencing the
target directly (media item / collection / smart / multi / rerun /
playlist); no generated collection or playlist. Response PlaylistId
is null.
- Multi-item lineup (>= 2): one generated IsSystem Playlist in a
get-or-created IsSystem PlaylistGroup ("Channel Lineups"), one
PlaylistItem per entry in lineup order with PlayAll=true, referenced
by a single Flood schedule item. Rerun collections and playlists are
rejected (422) in multi lineups (PlaylistItem/CollectionKey lack the
fields to enumerate them).
Review fixes:
- Normalize + strict-validate MediaType<->CollectionType pairs once up
front (422 on mismatch / wrong id / not exactly one id).
- Reject MultiCollection with non-Shuffle order (mirrors
PlayoutModeMustBeValid), Mirror playout source, all via 422.
- OnDemand parity: queue TimeShiftOnDemandPlayout post-commit.
- De-collide generated ProgramSchedule and Playlist names against their
unique indexes instead of leaking a UNIQUE-constraint DbUpdateException.
- Generic 422 on save failure + ILogger; AnyAsync existence checks;
Either/Validation unwrap via Match; XML doc on request DTO + endpoint.
- Response model: ChannelId, PlaylistId (nullable), ProgramScheduleId,
PlayoutId (CollectionId removed). Regenerated OpenAPI v1.json + v1.d.ts.
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>
ChannelResponseModel and BulkChannelRequests DTOs lacked file-scoped
#nullable enable, so the OpenAPI generator emitted every string
property as a ["null","string"] union (project Nullable default is
disable), polluting SPA typegen with needless `| null` unions.
ChannelStateResponseModel already complied with this convention
(precedent: PlayoutResponseModel). All properties in both files are
always populated (never null in practice), so no property needed to
become explicitly optional/nullable - just adding #nullable enable
was sufficient to clean up the generated schema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review fixes for the #97 endpoint:
- nowPlaying now resolves like the XMLTV guide: when the wall-clock item
is pre/mid-roll filler, surface the guide group's program item (title +
full program bounds) instead of the filler; a guide entry with no
program item (e.g. fallback loop) reports null. Previously an ad break
would retitle the channel and reset progress every 30 seconds.
- covering-item lookup now runs one cheap projected query per distinct
playout offset instead of hydrating the full metadata include tree for
a window widened by the largest offset across ALL channels (up to 24h);
metadata is fetched in a second query scoped to the covering guide
groups, time-bounded to stay safe against GuideGroup recycling (mod
10000). Regression caught while testing: covering items are keyed by
(source channel, lookup time) so a source channel no longer inherits
its offset mirror's lookup result.
- GetChannelStatesForApi now carries Now explicitly (controller passes
DateTime.UtcNow), making boundary semantics testable: new tests pin
now == Start (playing) and now == Finish (not playing), plus mid-roll,
filler-only, and mirror-source-row cases.
- ChannelNowPlayingResponseModel title is non-nullable in the schema
(#nullable enable) — GetDisplayTitle always returns a string.
- GetState uses the typed-return controller convention; Playouts.Mapper
reverted to internal (handler lives in the same assembly).
- Spec doc notes the OnDemand drift caveat and filler semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An off-air channel has no current playout item, so the handler returns
null for NowPlaying; the DTO now declares that (file-scoped #nullable —
ErsatzTV.Core has NRT disabled project-wide). Regenerated OpenAPI and
web types: nowPlaying keeps its nullable oneOf schema (matching the
schema contract test) and channelNumber loses a spurious nullable
annotation left over from an earlier draft — the field is required.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>