The UI half of #70 (backend + API shipped in PR #402). Pure SPA + docs — no new
endpoint/DTO (`weight` was already on MultiCollectionItemRequest/Response, and
`WeightedShuffle` already in the PlaybackOrder enum).
- Multi-collection editor (`/app/multi-collections`): per-source weight input
(1..1000, mirroring the API validator), a computed % share (3:1 shows 75/25),
and a "Reset to fair share" action (fair-share = all weights 1, decisions.md
2026-07-17 — a reset, not a separate mode). Weight round-trips through the
draft (read in itemsFromMultiCollection, written in toItemRequest) so the
replace-all PUT never silently resets it.
- Classic schedule editor: `WeightedShuffle` offered as a Playback Order ONLY
for MultiCollection sources (itemRules `MULTI_COLLECTION_ORDERS`) — it needs
per-source weights and the write path rejects it elsewhere. Excluded from
fillWithGroup like ShuffleInOrder (PlayoutBuilder schedules fill-groups
per-group, incompatible with whole-collection weighted share).
- `Input` gained min/max/inputMode/onBlur passthroughs for bounded numeric
fields (reusable by #425's weight UI); weight held as a string for smooth
editing, clamped on blur and at save so an out-of-range value never 400s.
- Docs: domain-model + spa-conventions (replace-all round-trip trap, bounded
numeric input pattern) + decisions.md entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cold-review findings:
- #1 (CONFIRMED): deep-linking/reloading into a route inside a default-collapsed
labeled group (e.g. /app/settings, /app/libraries) left the expanded sidebar
with no active indicator, violating "active route marked in both states". Now
the group that contains the active route is force-shown (and its header shows
expanded) WITHOUT persisting — navigating away reverts to the stored preference.
Regression test added.
- #2 (mobile): a persisted-collapsed state hid the brand logo+wordmark while the
toggle is also hidden at <=980px, leaving an empty header. Restore the brand in
the mobile query (collapse is desktop-only).
Verified live (Playwright): /app/settings deep-link shows Settings active +
aria-current, System auto-revealed, Media stays collapsed, nothing persisted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two independent, persisted collapse states to the shell sidebar:
- a header toggle that collapses it to a 60px icon rail
- collapsible accordions per labeled nav group (Media, System);
the unlabeled Primary group is always open, default-collapsed groups
State + persistence live in web/src/app/sidebarState.ts (useSidebarState);
AppShell stamps ctv-app-shell-collapsed on the shell root and the rail look
is CSS-driven. Two namespaced localStorage keys (ctv-sidebar-collapsed,
ctv-sidebar-groups) per the persisted-UI-state convention. In the rail,
accordions are ignored (all items shown icon-only, label kept in the a11y
tree + surfaced as a title tooltip, badges as a corner dot); active-route
indicator works in both states; grid-column transition respects
prefers-reduced-motion.
Tests: colocated sidebarState.test.ts + a new describe in App.test.tsx
(default-collapsed, accordion toggle+persist, rail, reload persistence).
Docs: spa-conventions §13 + decisions.md 2026-07-18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the ~14 health checks actionable: each check that has a fix now declares
where to go, and the SPA acts on it.
Backend:
- Widen domain HealthCheckLink (string Link) -> (string Target, HealthCheckLinkKind
Kind) with ExternalDoc|AppRoute + factories; only the 4 link-building checks and
the API mapper touched .Link.
- Evolve HealthCheckResponseModel additively (/api/v1 frozen-additive): keep
deprecated string? Link (still populated), add Brief (the BriefMessage the mapper
was silently dropping) and nested Remediation {Kind, Target}. Kind is a mapped
string, not a wire enum.
- Make Mapper.GetStatus total: NotApplicable no longer throws (defensive; handler
still filters it). InternalsVisibleTo(ErsatzTV.Tests) added to unit-test totality.
- Fix 2 stale Blazor route links (media/trash -> /app/trash, search?query ->
/app/search); add AppRoute remediation to actionable checks that had none
(libraries / schedules / ffmpeg-profiles / settings).
SPA:
- DashboardScreen health panel renders remediation: AppRoute -> client-side nav
button, ExternalDoc -> new-tab anchor; detail text truncates with title-hover.
- Remove the dead "Open Classic UI" -> /system/health row from SettingsScreen
(a #91b leftover that just 302'd to /app); update its regression test.
Docs: decisions.md (#164), api-conventions.md (deprecate-in-place DTO evolution),
blazor-route-parity.md (Section 4 correction); v1.json/v1.d.ts/endpoint-index
regenerated.
fixes#164
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two missing-base-reset defects in the ChicoryTV SPA shell, both confirmed by
rendering the real app (not source-reading):
- #373: the browser-default `body { margin: 8px }` was never reset, so it framed
every full-viewport layout — the app shell and the shell-less `.ctv-auth-page`
boot pages (login/setup/checking/error), both `min-height:100vh` — with a light
border on all four edges. Fix: `html, body { margin: 0 }` + paint the app surface
on `body` so any residual gap/overscroll stays dark (matches the design-system
reset in forms.card.html / chicorytv-admin templates). Verified body margin 0 and
no edge border on the boot page and the shell, warm theme, 1280px and 900px.
- #377: the SPA ships no global `box-sizing` reset (default content-box), so
`.ctv-nav-item { width:100%; padding:0 10px }` overflowed `.ctv-nav`
(`overflow:auto`) by 20px → a horizontal scrollbar in the sidebar. Fix: scope
`box-sizing: border-box` to `.ctv-nav-item`. Verified nav scrollWidth==clientWidth
(216==216) and no horizontal scroll after the fix; the intended vertical nav scroll
is unaffected.
Docs: spa-conventions.md §1 records the base reset and the deliberate absence of a
global box-sizing reset (set border-box locally when combining width:100% + padding).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the API-key SPA model with the session-cookie auth from PR1:
- client.ts: stop sending X-Api-Key; attach X-Csrf on mutating verbs (POST/PUT/
PATCH/DELETE); add suppressUnauthorizedSignal to skip the global 401 signal on
expected wrong-credentials 401s (login / change-password).
- api/auth.ts: hand-written wire types (AuthConfig/AuthSession/MachineKey — the
auth surface is IgnoreApi, deliberately not in generated types) + endpoint fns
(getAuthConfig/getAuthSession/login/setup/logout/changePassword/getMachineKey);
keep the notifyUnauthorized/subscribeUnauthorized 401 signal; add
clearLegacyStoredApiKey. Legacy get/set/clearStoredApiKey retained ONLY so the
still-shipping ApiKeyScreen (machine-key slice) compiles without a cross-slice
conflict — the client no longer reads them.
- AuthGate.tsx: boot gate wrapping <App/> outside the shell (mints no URL, deep
links survive login). checking -> setup | login | ready | error, with a safe
default AuthContext so App.test.tsx renders without a provider; config-fetch
failure lands on an explicit error+Retry, never a blank screen.
- LoginScreen / SetupScreen: shell-less centered cards; inline 401 / 409 handling;
SSO button + local-form gating from AuthConfig.
- UnauthorizedBanner: rewritten to prompt re-login (passive; consults the
unsaved-changes guard before flipping the gate).
- UserMenu: TopBar sign-out (guard -> logout -> signOut), mounted next to
ConnectMenu.
- main.tsx: wrap <AuthGate><App/></AuthGate> inside StrictMode.
Tests: client/auth/AuthGate/LoginScreen/SetupScreen/UnauthorizedBanner/UserMenu
(723 pass). Lint + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundle A SPA slice: the /api surface is now gated behind X-Api-Key on
every request (reads too, RequireKeyForReads defaults true), so a wrong/
missing key 401s everything.
- #282: send X-Api-Key on ALL requests when a key is stored, not only
mutations (removed the mutatingMethods split in api/client.ts).
- #280: new keyless API Key screen (/app/api-key, System nav) that reads/
writes only localStorage via auth.ts and never calls /api, so it works
on a fresh install where every read 401s. Masked key state, Save/Clear,
points at server-generated /config/api.key.
- 401 UX: client emits one app-wide unauthorized signal (auth.ts
notify/subscribeUnauthorized); a shell-level UnauthorizedBanner points
the user at the API Key screen. DRY, no per-screen 401 branches.
- Tests: inverted the GET header assertion (key now sent on reads), added
no-key and 401-signal client tests, auth signal tests, and screen +
banner tests. spa-conventions.md §5e documents the new seams.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChannelsScreen returned ChannelsEmptyState before the action bar that owns
"New blank channel", so a fresh install could never create its first channel.
The empty state now offers both create paths (bare-create + ChannelBuilder),
reusing the exact createBlankChannel handler (number = max+1 → 1 on empty,
group "ErsatzTV", default ffmpeg profile).
Regression: App.test.tsx bare-creates from a [] lineup, asserts the POST
payload (number "1") + navigation to /app/edit-channel/{id}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blazor parity for the remaining #213 conveniences:
- GET /api/logs gains sortField (timestamp|level) and sortDirection
(asc|desc) query params, allow-listed and normalized (unrecognized
values fall back to the pre-existing timestamp-desc default) rather
than rejected with a 422. LogsScreen.tsx renders clickable, sortable
column headers with a chevron direction indicator.
- LogsScreen.tsx now persists the chosen page size to localStorage
(ctv-logs-page-size) and restores it on mount, following the
existing designSystem.ts localStorage-preference pattern. This is a
client-local UI preference, not the Blazor ConfigElement-backed
server setting — see docs/decisions.md.
- TrashScreen.tsx adds a per-kind "See all N ..." affordance that
pages past the 100/kind /api/search cap using the already-paginated
GET /api/library/browse (mediaType + pageNum), appending results
client-side. No new API surface was needed since that endpoint
already supports the paging the trash screen needed.
docs/decisions.md, docs/blazor-route-parity.md, docs/spa-conventions.md
and docs/api-conventions.md updated in this same commit. OpenAPI spec
regenerated (v1.d.ts unchanged: query params aren't part of the
generated components/schemas surface).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review of #220 found in-grid episode card clicks never
scrolled/highlighted: navigateToPath() (routing.ts) does pushState +
a synthetic popstate, not a real hash change, so the anchor effect's
hashchange-only listener never fired for same-pathname navigation.
Now listens to both hashchange and popstate.
Also: track the last anchor value actually scrolled to so a
refetch/pagination that recreates the items array (anchor unchanged)
doesn't hijack scroll position; document the known CHILD_PAGE_SIZE
deep-link limitation (parity with the Blazor fragment link); and fix
the MediaPosterCard/shell.css comments that described the highlight
ring as "temporary" when only its glow pulse fades, not the ring
itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search and Media browse keep the previous successful result set rendered
during a refetch (query on Search; kind/query/page on Media browse) with no
gating, so per-card Add-to, Select/select-mode, the selection action bar, Add
all, and Save-as-smart-collection stayed live over stale, about-to-be-replaced
items. Worst path: SearchScreen.addAll only checked activeRef, so a late
GET /api/search/all-items could open a bulk-add dialog scoped to the previous
query's entire result set.
Key the success state to the request params that produced it and derive a
`refreshing` flag; while refreshing, keep cards visible but disable every
mutation surface, show a "Refreshing…" cue, and dim the grid. Card navigation
stays live. Bind addAll's completion to its query via lastQueryRef so a stale
all-items result is discarded. Same pattern applied to both screens.
Docs: spa-conventions §3a (refreshing/gating pattern) + §8 (temporal-semantics
review checklist); blazor-route-parity search/media-browse verdicts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Episode cards in the SPA search and media-browse screens were inert (mediaDetailPath had no
Episode case, and LibraryBrowseItemResponseModel carried no parent-season id to route with).
- API: add nullable SeasonId to LibraryBrowseItemResponseModel; populate it in
LibraryBrowseItemMapper.GetEpisodes (the single shared hydration site used by both the
library-browse search/browse handler and the season episode drill-in), leave it null for
every other kind. Regenerated v1.json + v1.d.ts per docs/api-conventions.md §5.
- SPA: mediaDetailPath now routes Episode items with a seasonId to
/app/media/seasons/{seasonId}#episode-{id} (matching Blazor's Search.razor:241 link), null
otherwise. MediaPosterCard accepts an id/highlighted pair; SeasonDetailScreen's episode grid
gives each card a stable `episode-{id}` anchor and scrolls/highlights it on mount and on
hashchange (deep-link support).
- Tests: GetLibraryBrowseItemsHandlerTests asserts SeasonId is populated for episode drill-in
results and null for other kinds; web tests cover mediaDetailPath's episode cases and the
anchor/scroll/highlight behavior (jsdom scrollIntoView stub).
- Docs: blazor-route-parity.md's episode-browse row and the Search cluster verdict updated —
the standalone SPA episode browse exists and episode cards now navigate, closing the
adversarial-reviewer#18 finding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the SPA screens over the new detail/info/image endpoints:
- Detail pages at /app/media/{movies|shows|seasons|artists}/{id}. MediaBrowseScreen
cards for movies/shows/artists (and season/show drill-in cards) navigate to them.
Shows list seasons -> seasons list episodes -> artists list music videos, each via
the browse parentId drill-in, paged. Layout: fanart/poster, title/year/plot, chip
lists, cast row, movie path + FileNotFound/Unavailable warnings. A "Media Info"
dialog (streams + chapters) backs onto GET /api/media-items/{id}/info. Add-to-
collection/playlist deferred to #153/#155 (TODO left in code).
- Image folder browser at /app/media/images/browser: lazy expandable tree, per-folder
image/subfolder counts + duration, edit dialog (PUT set/clear), and a per-folder
search link (library_folder_id:{id}). Reachable via a "Folder Browser" button on the
Images browse view.
New /app/media sub-paths are owned by a MediaRouteScreen wrapper that tracks pathname
locally + listens for popstate (App-level routing returns the same 'media' route object
for base and sub-paths). Client modules web/src/api/mediaDetail.ts + imageFolders.ts,
typed via the generated v1.d.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two new System-area screens over the new endpoints:
- /app/troubleshooting/blocks: pick a Block playout, browse its blocks
(grouped, name filter), drill into paged block history, click a row to
decode its details.
- /app/troubleshooting/yaml: paste YAML, toggle import schema, validate;
shows messages + JSON conversion.
Adds api/playoutHistory client + validateSequentialSchedule; regenerated
generated/v1.d.ts. Async loads follow the LogsScreen seq-guard pattern
(no synchronous setState inside effects).
Refs #145#158
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part of the #180 library picker fixes (SPA side).
Channel builder: TelevisionSeason is removed from the library grid fan-out, so a
multi-season show renders as one tile instead of one tile per season. Show tiles
gain a "Seasons" drill-in affordance (both grid and compact layouts) that opens a
dialog listing that show's seasons (via the new GET /api/library/browse?parentId=
&mediaType=TelevisionSeason), each with title, artwork and an Add button that
drops the specific season into the lineup.
Collections add-items dialog: the default search fan-out now excludes seasons,
and a media-kind filter row (All / Movies / Shows / Seasons / Artists,
default = All-without-seasons) keeps seasons reachable when explicitly selected.
Client: getLibraryBrowseItems gains an optional parentId param. Tests cover the
param mapping, the builder no longer requesting TelevisionSeason, and the
collections dialog default-excluding vs explicitly-including seasons.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds two sub-path editors under the Playouts screen:
- /app/playouts/{id}/alternate-schedules (Classic playouts)
- /app/playouts/{id}/templates (Block playouts)
Each has a reorderable priority table (up/down/delete) plus a selected-row
detail panel: schedule or template+deco-template pickers, a limit-to-date-range
toggle gating month/day/year selects, and day-of-week / day-of-month / month
multi-select chips with Weekdays/Weekends helpers. Entry points on the playout
card are kind-gated. The Block playout card also gets a default-deco select
wired to PUT /api/playouts/{id}/deco, using the new decoName read field.
DayOfWeek is overridden to day-name strings in the API client (the wire format
is Newtonsoft StringEnumConverter, though the OpenAPI schema types it as number).
Deviations from Blazor: chip multi-select instead of MudSelect, flat grouped
template pickers instead of a group->item cascade, a shorter year range, and the
template calendar preview is omitted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MediaBrowseScreen (/app/media): one parameterized screen for all 9 top-level
kinds with an in-screen kind switcher, per-kind search box, 100/page paging
- SearchScreen (/app/search): grouped per-kind results with counts + 'See all';
TopBar search input now navigates here
- TrashScreen (/app/trash): state:FileNotFound results, multi-select + Empty Trash
via DELETE /api/media-items and /api/maintenance/empty_trash (confirm dialogs)
- Shared media/mediaKinds (icon/label maps, hueOf, duration helpers) + MediaPosterCard;
ChannelBuilder and CollectionsScreen now reuse the single-source maps (their
exhaustive Record<LibraryBrowseMediaType> had to cover the 6 new kinds anyway)
- api: search.ts, mediaItems.ts, maintenance.ts (+ URL-assert tests)
- One consolidated 'Browse' nav entry with an in-screen kind switcher instead of
9 per-kind nav rows (deviation from Blazor's per-kind Media links)
- Nav: Media group gains Browse/Search/Trash; Settings Classic-UI help trimmed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add typed API wrappers (api/logs.ts, api/troubleshoot.ts) and two new
screens: LogsScreen (paged, level-badged, server-side filtered log table)
and TroubleshootingScreen (General JSON viewer with copy, plus per-platform
NVIDIA/QSV/VAAPI/VideoToolbox capability tabs when populated). Both are
registered under a new "System" nav group in App.tsx alongside Settings.
Settings' Classic UI help text and About card now point at the new screens
instead of the Blazor logs/troubleshooting pages.
Replaces the PlaceholderScreen at /app/collections with a real screen that
covers manual and smart collections, matching the SPA's extracted-screen
pattern (screens/CollectionsScreen.tsx; App.tsx gets only the route branch +
import).
Manual collections: list, create, rename, delete, and a per-row toggle for
UseCustomPlaybackOrder (PUT). Item management: an add-items picker that searches
the library (library/browse) and buckets results into the typed
AddItemsToCollectionRequest, plus per-item remove.
Smart collections: list, create, edit (name + query), delete, with a live
result preview that runs the query through library/browse.
API layer: new api/collections.ts owns the typed CRUD (getCollections/
getSmartCollections moved here from schedules.ts; schedules imports them). Unit
tests cover the client, the add-items bucket mapping, and the screen.
Honest gap: no API endpoint lists a manual collection's items by id. The items
view uses a best-effort collection:"name" Lucene search (movies/shows/seasons/
artists only) behind a prominent note. Multi/rerun/playlist collections are out
of scope (no API) and pointed to the Classic UI. Follow-ups #151/#152/#153.
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>
- 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>