feat(web): Schedule editor screen (#86) #125

Merged
timothy merged 2 commits from feat/86-schedule-editor into main 2026-07-05 12:22:16 +02:00
Owner

Summary

Implements issue #86 by replacing the Schedules placeholder with the ChicoryTV schedule editor SPA screen wired to the live Schedule API.

  • Adds web/src/api/schedules.ts as a thin generated-type API wrapper/query hook.
  • Adds the two-pane schedule item lineup + tabbed inspector in web/src/App.tsx.
  • Persists drag/drop and keyboard up/down reorder through PUT /api/schedules/{id}/items replace-all.
  • Adds mutation error banners, disabled-while-mutating controls, retry/loading/empty states, and picker sorting for DB-order filler/watermark lists.
  • Uses dependency-free drag/drop and keyboard reorder; no @dnd-kit dependency added.

Prototype/API Mapping

Prototype surface Endpoint / DTO Scope
Schedule list/header GET /api/schedules, ProgramScheduleViewModel In scope
Schedule item list GET /api/schedules/{id}/items, ProgramScheduleItemsWithDurationViewModel, ProgramScheduleItemViewModel In scope
Drag + keyboard reorder PUT /api/schedules/{id}/items, ReplaceScheduleItemsRequest, ScheduleItemRequest In scope
Add collection-backed item POST /api/schedules/{id}/items, ScheduleItemRequest In scope, conservative first-collection default
Delete item DELETE /api/schedules/{id}/items/{itemId} In scope
Collection picker GET /api/collections, MediaCollectionViewModel In scope
Smart collection picker GET /api/smart-collections, SmartCollectionViewModel In scope
Filler presets GET /api/filler-presets, FillerPresetResponseModel In scope, client-sorted
Watermarks GET /api/watermarks, WatermarkResponseModel In scope, client-sorted
Duration estimates durationEstimate, totalDurationEstimate from generated schedule DTOs In scope; null renders as unknown
Preferred language fields No languages-list endpoint yet Deferred, disabled controls with note
Schedule CRUD management POST/PUT/DELETE /api/schedules Out of scope for this editor pass
Preview/live playback surface No requested schedule preview endpoint Deferred/out of scope, same pattern as Channels preview deferral

Verification

  • cd web && npm ci passed.
  • cd web && npm run lint passed.
  • cd web && npm run typecheck passed.
  • cd web && npm test -- --run passed: 4 files, 50 tests.
  • cd web && npm run build passed.
  • TZ=UTC dotnet build ErsatzTV.sln passed with existing NU1903/CS8604 warnings.
  • TZ=UTC dotnet test ErsatzTV.Tests --no-build passed: 368 tests.
  • TZ=UTC dotnet test ErsatzTV.Core.Tests --no-build passed: 493 passed, 1 skipped.

Deferred / Uncertain

  • Languages list remains deferred because the API still has no languages-list endpoint.
  • Multi-collection picker is not wired as a separate selector on this base; existing ProgramScheduleItemViewModel values are rendered when present.
  • Schedule CRUD controls are intentionally not implemented here; this screen focuses on item editing/reorder.

Reviewer Notes

Subagents were used for independent API-contract and Channels-pattern reconnaissance before implementation. Finding classification for Fable: NITS may be fixed directly on this branch; SUBSTANTIAL findings should come back to Codex or be fixed in-session with subagents, then user decides.

## Summary Implements issue #86 by replacing the Schedules placeholder with the ChicoryTV schedule editor SPA screen wired to the live Schedule API. - Adds `web/src/api/schedules.ts` as a thin generated-type API wrapper/query hook. - Adds the two-pane schedule item lineup + tabbed inspector in `web/src/App.tsx`. - Persists drag/drop and keyboard up/down reorder through `PUT /api/schedules/{id}/items` replace-all. - Adds mutation error banners, disabled-while-mutating controls, retry/loading/empty states, and picker sorting for DB-order filler/watermark lists. - Uses dependency-free drag/drop and keyboard reorder; no `@dnd-kit` dependency added. ## Prototype/API Mapping | Prototype surface | Endpoint / DTO | Scope | | --- | --- | --- | | Schedule list/header | `GET /api/schedules`, `ProgramScheduleViewModel` | In scope | | Schedule item list | `GET /api/schedules/{id}/items`, `ProgramScheduleItemsWithDurationViewModel`, `ProgramScheduleItemViewModel` | In scope | | Drag + keyboard reorder | `PUT /api/schedules/{id}/items`, `ReplaceScheduleItemsRequest`, `ScheduleItemRequest` | In scope | | Add collection-backed item | `POST /api/schedules/{id}/items`, `ScheduleItemRequest` | In scope, conservative first-collection default | | Delete item | `DELETE /api/schedules/{id}/items/{itemId}` | In scope | | Collection picker | `GET /api/collections`, `MediaCollectionViewModel` | In scope | | Smart collection picker | `GET /api/smart-collections`, `SmartCollectionViewModel` | In scope | | Filler presets | `GET /api/filler-presets`, `FillerPresetResponseModel` | In scope, client-sorted | | Watermarks | `GET /api/watermarks`, `WatermarkResponseModel` | In scope, client-sorted | | Duration estimates | `durationEstimate`, `totalDurationEstimate` from generated schedule DTOs | In scope; null renders as unknown | | Preferred language fields | No languages-list endpoint yet | Deferred, disabled controls with note | | Schedule CRUD management | `POST/PUT/DELETE /api/schedules` | Out of scope for this editor pass | | Preview/live playback surface | No requested schedule preview endpoint | Deferred/out of scope, same pattern as Channels preview deferral | ## Verification - `cd web && npm ci` passed. - `cd web && npm run lint` passed. - `cd web && npm run typecheck` passed. - `cd web && npm test -- --run` passed: 4 files, 50 tests. - `cd web && npm run build` passed. - `TZ=UTC dotnet build ErsatzTV.sln` passed with existing NU1903/CS8604 warnings. - `TZ=UTC dotnet test ErsatzTV.Tests --no-build` passed: 368 tests. - `TZ=UTC dotnet test ErsatzTV.Core.Tests --no-build` passed: 493 passed, 1 skipped. ## Deferred / Uncertain - Languages list remains deferred because the API still has no languages-list endpoint. - Multi-collection picker is not wired as a separate selector on this base; existing `ProgramScheduleItemViewModel` values are rendered when present. - Schedule CRUD controls are intentionally not implemented here; this screen focuses on item editing/reorder. ## Reviewer Notes Subagents were used for independent API-contract and Channels-pattern reconnaissance before implementation. Finding classification for Fable: NITS may be fixed directly on this branch; SUBSTANTIAL findings should come back to Codex or be fixed in-session with subagents, then user decides.
timothy added 1 commit 2026-07-05 11:43:48 +02:00
feat(web): add schedule editor screen
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2e58fa1815
Author
Owner

Fable review — PR #125 (head 2e58fa18), read-only, 3-lens subagent review

Verdict: APPROVE with findings — no blockers after verification. 3 SUBSTANTIAL + several nits; per user instruction ALL findings are being fixed on this branch in-session (subagents), fixes to follow as a commit.

The interesting one: suspected reorder blocker — REFUTED, downgraded to a contract-fragility follow-up (#126)

One reviewer lens claimed reorder would 422 on any schedule containing Multiple/Duration items because the derived-type fields (count, multipleMode, playoutDuration, tailMode, discardToFillAttempts) are absent from the generated OpenAPI schema (abstract base VM, no polymorphism annotation) and the PR compensates with a hand-widened TS type. A second lens checked Startup.cs:343: the API serializes with Newtonsoft by runtime type, so those fields ARE on the wire and the widening is a correct compensation — verified inline; no runtime bug, no data loss (all 36 ScheduleItemRequest fields round-trip 1:1). BUT the OpenAPI document is wrong today and a future serializer change would silently break reorder → filed #126 (flatten to a real API DTO, then drop the widened type).

SUBSTANTIAL (being fixed)

  1. Only schedules[0] is ever loaded — no schedule selector anywhere; users with >1 schedule can never edit the others.
  2. useScheduleScreenQuery mounts with a duplicate copy-pasted effect → the full 6-endpoint screen fetch fires TWICE in parallel on every mount.
  3. "Add item" silently attaches to the alphabetically-first collection with no way to choose or correct it (inspector is read-only) → needs a collection picker in the add flow.

NITS (being fixed)

  • Inert header "Save" button (no onClick; persistence is per-mutation) — remove.
  • refresh() after every mutation refetches all 4 picker endpoints; reorder should reconcile from the PUT response, add/delete should refetch items only.
  • moveItem's if (mutating) guard reads render-closure state — rapid double-click can launch overlapping replace-all PUTs.
  • Hand-widened type needs a comment pointing at #126 (it silently defeats the generated contract otherwise).
  • Prototype fidelity: Filler tab missing the Tail row; Content tab missing "Keep multi-part together" (both silently absent rather than visibly deferred).
  • a11y: aria-pressed on the item-select button should be aria-current; no draggable role description.
  • CSS: var(--text-md, 15px) fallback wrong (token is 14px, ×2); var(--text-faint) token doesn't exist in the design system (pre-existing convention bug, fixing new occurrences).
  • Tests: unreachable /api/schedules/{id} mock branch; collections fixture omits required MediaCollectionViewModel fields (needs a collection() helper); picker-fetch failure path untested.

Verified clean

Endpoint paths/methods/status codes for all 10 endpoints match backend controllers exactly; unpaged list shapes correct; durationEstimate/totalDurationEstimate names+nullability exact; all enum string literals match v1.d.ts; hook ordering/unmount guards; id-based selection fallback after delete; drag handler hygiene; empty/null-duration rendering; no new npm deps; scope clean (5 files, no backend changes); component prop contracts all real; two-pane layout, 4-tab inspector, drag affordances and duration formatting match the prototype; keyboard reorder present, end-disabled, and tested.

## Fable review — PR #125 (head 2e58fa18), read-only, 3-lens subagent review **Verdict: APPROVE with findings — no blockers after verification. 3 SUBSTANTIAL + several nits; per user instruction ALL findings are being fixed on this branch in-session (subagents), fixes to follow as a commit.** ### The interesting one: suspected reorder blocker — REFUTED, downgraded to a contract-fragility follow-up (#126) One reviewer lens claimed reorder would 422 on any schedule containing Multiple/Duration items because the derived-type fields (`count`, `multipleMode`, `playoutDuration`, `tailMode`, `discardToFillAttempts`) are absent from the generated OpenAPI schema (abstract base VM, no polymorphism annotation) and the PR compensates with a hand-widened TS type. A second lens checked `Startup.cs:343`: the API serializes with **Newtonsoft by runtime type**, so those fields ARE on the wire and the widening is a correct compensation — verified inline; no runtime bug, no data loss (all 36 `ScheduleItemRequest` fields round-trip 1:1). BUT the OpenAPI document is wrong today and a future serializer change would silently break reorder → filed **#126** (flatten to a real API DTO, then drop the widened type). ### SUBSTANTIAL (being fixed) 1. Only `schedules[0]` is ever loaded — no schedule selector anywhere; users with >1 schedule can never edit the others. 2. `useScheduleScreenQuery` mounts with a duplicate copy-pasted effect → the full 6-endpoint screen fetch fires TWICE in parallel on every mount. 3. "Add item" silently attaches to the alphabetically-first collection with no way to choose or correct it (inspector is read-only) → needs a collection picker in the add flow. ### NITS (being fixed) - Inert header "Save" button (no onClick; persistence is per-mutation) — remove. - `refresh()` after every mutation refetches all 4 picker endpoints; reorder should reconcile from the PUT response, add/delete should refetch items only. - `moveItem`'s `if (mutating)` guard reads render-closure state — rapid double-click can launch overlapping replace-all PUTs. - Hand-widened type needs a comment pointing at #126 (it silently defeats the generated contract otherwise). - Prototype fidelity: Filler tab missing the Tail row; Content tab missing "Keep multi-part together" (both silently absent rather than visibly deferred). - a11y: `aria-pressed` on the item-select button should be `aria-current`; no draggable role description. - CSS: `var(--text-md, 15px)` fallback wrong (token is 14px, ×2); `var(--text-faint)` token doesn't exist in the design system (pre-existing convention bug, fixing new occurrences). - Tests: unreachable `/api/schedules/{id}` mock branch; collections fixture omits required `MediaCollectionViewModel` fields (needs a `collection()` helper); picker-fetch failure path untested. ### Verified clean Endpoint paths/methods/status codes for all 10 endpoints match backend controllers exactly; unpaged list shapes correct; `durationEstimate`/`totalDurationEstimate` names+nullability exact; all enum string literals match v1.d.ts; hook ordering/unmount guards; id-based selection fallback after delete; drag handler hygiene; empty/null-duration rendering; no new npm deps; scope clean (5 files, no backend changes); component prop contracts all real; two-pane layout, 4-tab inspector, drag affordances and duration formatting match the prototype; keyboard reorder present, end-disabled, and tested.
timothy added 1 commit 2026-07-05 12:16:56 +02:00
fix(web): schedule editor review fixes for #86
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
f18f2dc058
- 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>
timothy scheduled this pull request to auto merge when all checks succeed 2026-07-05 12:19:40 +02:00
timothy merged commit 229f7cc1b0 into main 2026-07-05 12:22:16 +02:00
Sign in to join this conversation.