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.
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)
Only schedules[0] is ever loaded — no schedule selector anywhere; users with >1 schedule can never edit the others.
useScheduleScreenQuery mounts with a duplicate copy-pasted effect → the full 6-endpoint screen fetch fires TWICE in parallel on every mount.
"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).
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.
- 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 main2026-07-05 12:22:16 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements issue #86 by replacing the Schedules placeholder with the ChicoryTV schedule editor SPA screen wired to the live Schedule API.
web/src/api/schedules.tsas a thin generated-type API wrapper/query hook.web/src/App.tsx.PUT /api/schedules/{id}/itemsreplace-all.@dnd-kitdependency added.Prototype/API Mapping
GET /api/schedules,ProgramScheduleViewModelGET /api/schedules/{id}/items,ProgramScheduleItemsWithDurationViewModel,ProgramScheduleItemViewModelPUT /api/schedules/{id}/items,ReplaceScheduleItemsRequest,ScheduleItemRequestPOST /api/schedules/{id}/items,ScheduleItemRequestDELETE /api/schedules/{id}/items/{itemId}GET /api/collections,MediaCollectionViewModelGET /api/smart-collections,SmartCollectionViewModelGET /api/filler-presets,FillerPresetResponseModelGET /api/watermarks,WatermarkResponseModeldurationEstimate,totalDurationEstimatefrom generated schedule DTOsPOST/PUT/DELETE /api/schedulesVerification
cd web && npm cipassed.cd web && npm run lintpassed.cd web && npm run typecheckpassed.cd web && npm test -- --runpassed: 4 files, 50 tests.cd web && npm run buildpassed.TZ=UTC dotnet build ErsatzTV.slnpassed with existing NU1903/CS8604 warnings.TZ=UTC dotnet test ErsatzTV.Tests --no-buildpassed: 368 tests.TZ=UTC dotnet test ErsatzTV.Core.Tests --no-buildpassed: 493 passed, 1 skipped.Deferred / Uncertain
ProgramScheduleItemViewModelvalues are rendered when present.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.
Fable review — PR #125 (head
2e58fa18), read-only, 3-lens subagent reviewVerdict: 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 checkedStartup.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 36ScheduleItemRequestfields 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)
schedules[0]is ever loaded — no schedule selector anywhere; users with >1 schedule can never edit the others.useScheduleScreenQuerymounts with a duplicate copy-pasted effect → the full 6-endpoint screen fetch fires TWICE in parallel on every mount.NITS (being fixed)
refresh()after every mutation refetches all 4 picker endpoints; reorder should reconcile from the PUT response, add/delete should refetch items only.moveItem'sif (mutating)guard reads render-closure state — rapid double-click can launch overlapping replace-all PUTs.aria-pressedon the item-select button should bearia-current; no draggable role description.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)./api/schedules/{id}mock branch; collections fixture omits requiredMediaCollectionViewModelfields (needs acollection()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/totalDurationEstimatenames+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.