feat(web): Libraries screen (#88) #128

Merged
timothy merged 2 commits from feat/88-libraries into main 2026-07-05 19:06:51 +02:00
Owner

Summary

Implements the ChicoryTV Libraries screen from design-system/templates/chicorytv-admin/ScheduleLibrary.jsx, wired to the live media-source and library scan APIs.

Mapping / Scope

Prototype need Live API mapping Notes
Media source cards GET /api/media-sources -> MediaSourceResponseModel[] Exact kind values from backend mapper: Local, Plex, Jellyfin, Emby; connectionAddress nullable.
Library rows MediaSourceResponseModel.libraries[] { id, name, mediaKind, lastScan, itemCount }; tests use generated/backend enum values such as Movies, Shows, OtherVideos, MusicVideos.
Scan progress GET /api/libraries/scan-status -> LibraryScanStatusResponseModel[] Polls only while at least one scan-status entry exists, at a minimum 10s interval; stops when no scans remain and refreshes sources once on completion.
Trigger scan POST /api/libraries/{id}/scan Uses no request body; handles 200 OK success and bare 404 failure. Global write-key ProblemDetails still surfaces via ApiError.detail when present.
Scan show POST /api/libraries/{id}/scan-show Not wired because the prototype has no show-title workflow.

Visible deferrals: Add Source, Scan All, and source settings are disabled because the current API surface does not provide those SPA workflows.

Files changed

  • web/src/api/libraries.ts
  • web/src/api/index.ts
  • web/src/App.tsx
  • web/src/App.test.tsx
  • web/src/shell.css

Verification

  • cd web && npm ci passed
  • cd web && npm run lint passed
  • cd web && npm run typecheck passed
  • cd web && npm test -- --run passed: 70 tests
  • cd web && npm run build passed
  • TZ=UTC dotnet build ErsatzTV.sln passed (existing NuGet/nullability warnings only)
  • TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-build passed: 368 tests
  • TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-build passed: 493 passed, 1 skipped

Closes #88

## Summary Implements the ChicoryTV Libraries screen from `design-system/templates/chicorytv-admin/ScheduleLibrary.jsx`, wired to the live media-source and library scan APIs. ## Mapping / Scope | Prototype need | Live API mapping | Notes | | --- | --- | --- | | Media source cards | `GET /api/media-sources` -> `MediaSourceResponseModel[]` | Exact `kind` values from backend mapper: `Local`, `Plex`, `Jellyfin`, `Emby`; `connectionAddress` nullable. | | Library rows | `MediaSourceResponseModel.libraries[]` | `{ id, name, mediaKind, lastScan, itemCount }`; tests use generated/backend enum values such as `Movies`, `Shows`, `OtherVideos`, `MusicVideos`. | | Scan progress | `GET /api/libraries/scan-status` -> `LibraryScanStatusResponseModel[]` | Polls only while at least one scan-status entry exists, at a minimum 10s interval; stops when no scans remain and refreshes sources once on completion. | | Trigger scan | `POST /api/libraries/{id}/scan` | Uses no request body; handles 200 OK success and bare 404 failure. Global write-key ProblemDetails still surfaces via `ApiError.detail` when present. | | Scan show | `POST /api/libraries/{id}/scan-show` | Not wired because the prototype has no show-title workflow. | Visible deferrals: `Add Source`, `Scan All`, and source settings are disabled because the current API surface does not provide those SPA workflows. ## Files changed - `web/src/api/libraries.ts` - `web/src/api/index.ts` - `web/src/App.tsx` - `web/src/App.test.tsx` - `web/src/shell.css` ## Verification - `cd web && npm ci` passed - `cd web && npm run lint` passed - `cd web && npm run typecheck` passed - `cd web && npm test -- --run` passed: 70 tests - `cd web && npm run build` passed - `TZ=UTC dotnet build ErsatzTV.sln` passed (existing NuGet/nullability warnings only) - `TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-build` passed: 368 tests - `TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-build` passed: 493 passed, 1 skipped Closes #88
timothy added 1 commit 2026-07-05 18:26:36 +02:00
feat(web): add Libraries screen refs #88
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
5c4acb9c7b
Author
Owner

Fable review — PR #128 (head 5c4acb9c), read-only, 3-lens subagent review

Verdict: APPROVE with findings — no blockers, but 2 production-visible SUBSTANTIALS in the scan flow + 1 substantial fidelity drop + nits. Notably the cleanest PR of the series on contracts and process: mapper-exact kind fixtures, honest bare-404 scan test, mount-delta isolation, genuine poll stop/refetch-once transition test, responsive collapse present, a11y solid.

SUBSTANTIAL

  1. Scan progress renders ~0% for the whole scan: the wire value of LibraryScanStatusResponseModel.percent is a 0–1 fraction (traced end-to-end: scanner computes fractions, proxy/handler never ×100) but the UI feeds it to ProgressBar (clamped 0–100) — a half-done scan shows a 0.5% bar. Tests mask it with invented 0–100 fixtures (the #109/#127 class). Fix: ×100 in the frontend + fractional wire-value fixtures; backend field name/scale wart noted for the backlog.
  2. Polling frequently never starts after triggering a scan: POST /scan returns 200 on queueing, but scan-status only reports scans the scanner actually started (guaranteed seconds of lag on Plex/Jellyfin/Emby — a full libraries-sync runs first). The single immediate status fetch lands in that gap → the just-triggered id is pruned from scanningLibraryIds (button re-enables mid-scan) and the scanStatuses.length === 0 guard never starts the interval → no progress, no completion refresh until manual reload. Fix: pending-ids set that survives pruning until seen active (or grace window); poll while pending ∪ active is nonempty.
  3. Per-source "Scan" action silently dropped: the prototype's source-card header has Scan + Settings; the PR ships only Settings (visibly deferred). Needs the visible-deferral treatment (or wiring it to scan all the source's libraries).

NITS

  1. The media-sources-500 test invents a ProblemDetails body the backend can't produce (no exception middleware exists) — should be a bare 500 asserting the generic fallback, the exact honest pattern the PR itself uses for the scan-trigger 404.
  2. No fractional/0/100 percent edge tests (backend emits decimals like 62.5... of a fraction — folds into fix 1's fixture correction).
  3. await loadScanStatuses() awaits undefined (callback returns void) — misleading no-op.
  4. Poll effect depends on state → the interval is torn down and recreated every tick; should depend on a derived hasActiveScans boolean.
  5. triggerScan lacks the ref-based double-submit guard (#125/#127 convention) — and finding 2's prune race actively re-enables the button.
  6. CSS: --radius-xs fallback 4px vs real 3px (every other usage in the file says 3px).
  7. Source-card StatusDot conveys scanning/ok by color alone (no label/text) — the per-library rows pair it with a Badge; the source-level dot should too (or use StatusDot's label).

Verified clean

Endpoints/DTO shapes exact vs backend; kind strings match GetAllMediaSourcesForApiHandler exactly; scan POST bare-200/404 handling precisely matches the controller; mount discipline (one sources + one status fetch, StrictMode-safe); poll stops on empty and the completion source-refetch fires exactly once; failed poll ticks don't kill the interval; no fetches in setState updaters; orphan status rows safe; concurrent scans compose; responsive collapse present for all new grids; no phantom component props; no new deps; scope = 5 declared files; scan-show genuinely absent from the prototype (not a drop); per-library buttons and progress bars properly labeled.

## Fable review — PR #128 (head 5c4acb9c), read-only, 3-lens subagent review **Verdict: APPROVE with findings — no blockers, but 2 production-visible SUBSTANTIALS in the scan flow + 1 substantial fidelity drop + nits. Notably the cleanest PR of the series on contracts and process: mapper-exact `kind` fixtures, honest bare-404 scan test, mount-delta isolation, genuine poll stop/refetch-once transition test, responsive collapse present, a11y solid.** ### SUBSTANTIAL 1. **Scan progress renders ~0% for the whole scan**: the wire value of `LibraryScanStatusResponseModel.percent` is a **0–1 fraction** (traced end-to-end: scanner computes fractions, proxy/handler never ×100) but the UI feeds it to `ProgressBar` (clamped 0–100) — a half-done scan shows a 0.5% bar. Tests mask it with invented 0–100 fixtures (the #109/#127 class). Fix: ×100 in the frontend + fractional wire-value fixtures; backend field name/scale wart noted for the backlog. 2. **Polling frequently never starts after triggering a scan**: POST /scan returns 200 on *queueing*, but scan-status only reports scans the scanner actually *started* (guaranteed seconds of lag on Plex/Jellyfin/Emby — a full libraries-sync runs first). The single immediate status fetch lands in that gap → the just-triggered id is pruned from `scanningLibraryIds` (button re-enables mid-scan) and the `scanStatuses.length === 0` guard never starts the interval → no progress, no completion refresh until manual reload. Fix: pending-ids set that survives pruning until seen active (or grace window); poll while `pending ∪ active` is nonempty. 3. **Per-source "Scan" action silently dropped**: the prototype's source-card header has Scan + Settings; the PR ships only Settings (visibly deferred). Needs the visible-deferral treatment (or wiring it to scan all the source's libraries). ### NITS 4. The media-sources-500 test invents a ProblemDetails body the backend can't produce (no exception middleware exists) — should be a bare 500 asserting the generic fallback, the exact honest pattern the PR itself uses for the scan-trigger 404. 5. No fractional/0/100 percent edge tests (backend emits decimals like 62.5... of a fraction — folds into fix 1's fixture correction). 6. `await loadScanStatuses()` awaits `undefined` (callback returns void) — misleading no-op. 7. Poll effect depends on `state` → the interval is torn down and recreated every tick; should depend on a derived `hasActiveScans` boolean. 8. `triggerScan` lacks the ref-based double-submit guard (#125/#127 convention) — and finding 2's prune race actively re-enables the button. 9. CSS: `--radius-xs` fallback 4px vs real 3px (every other usage in the file says 3px). 10. Source-card `StatusDot` conveys scanning/ok by color alone (no label/text) — the per-library rows pair it with a Badge; the source-level dot should too (or use StatusDot's `label`). ### Verified clean Endpoints/DTO shapes exact vs backend; `kind` strings match `GetAllMediaSourcesForApiHandler` exactly; scan POST bare-200/404 handling precisely matches the controller; mount discipline (one sources + one status fetch, StrictMode-safe); poll stops on empty and the completion source-refetch fires exactly once; failed poll ticks don't kill the interval; no fetches in setState updaters; orphan status rows safe; concurrent scans compose; responsive collapse present for all new grids; no phantom component props; no new deps; scope = 5 declared files; scan-show genuinely absent from the prototype (not a drop); per-library buttons and progress bars properly labeled.
timothy added 1 commit 2026-07-05 19:01:08 +02:00
fix(web): libraries scan-flow review fixes for #88
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
bee686c846
- 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>
timothy merged commit d63719ece9 into main 2026-07-05 19:06:51 +02:00
timothy deleted branch feat/88-libraries 2026-07-05 19:06:51 +02:00
Sign in to join this conversation.