---
key: spa.schedules-editor-draft-save
title: '2026-07-11 — Schedules SPA editor: draft/explicit-Save over instant-persist; Copy includes multi/smart/rerun; shuffled-GET normalization preserved'
status: active
since: '2026-07-11'
supersedes: none
superseded-by: none
rule: The schedules SPA editor mutates a local draft and flushes one explicit Save (`PUT /api/schedules/{id}/items`) instead of instant-persisting each action; Copy deep-copies all source references (fixing a Blazor omission); the shuffled-schedule GET's `EnforceProperties` lossy normalization is preserved and mirrored in the SPA's option lists.
signals: 'draft/dirty guard, `navigationGuard.ts`, `copyDraftItem`, `EnforceProperties` · paths: `web/src/screens/SchedulesScreen.tsx`, `web/src/schedules/`, `docs/spa-conventions.md` §8 · issues: #207'
mechanics: '`docs/spa-conventions.md` §8'
---

The ChicoryTV schedules editor (`web/src/screens/SchedulesScreen.tsx` + `web/src/schedules/`,
issue #207) rebuilds the schedule-item lineup to full mutation parity with the legacy Blazor editor.
Three deliberate decisions:

**(a) Draft model with one explicit Save, replacing instant-persist.** All add/edit/copy/remove/
reorder mutate a **local draft list only**; a single **Save** issues one
`PUT /api/schedules/{id}/items` (the replace endpoint). This is intentional because that PUT is
**destructive server-side** — it deletes and recreates every item row (new ids) and triggers playout
rebuilds — so batching edits into one flush (vs. the old per-action POST/DELETE/PUT) minimizes churn
and gives the user a Discard/dirty affordance. On 422/network error the draft is kept and the error
surfaced; on success the draft is replaced with the server response. A **Discard** action and a dirty
guard (native `confirm()` on schedule-switch + in-app nav via `navigationGuard.ts`, plus a
`beforeunload` listener) protect the draft. See `docs/spa-conventions.md` §8. This retires the old
inline `ScheduleScreen` in `App.tsx` and its instant-persist add/delete/reorder tests.

**(b) Copy item deep-copies ALL source references, including multi/smart/rerun collections.** Blazor's
`CopyItem` omitted the multi-collection / smart-collection / rerun-collection references when
duplicating an item (copying only the plain collection/media-item/playlist refs) — a latent bug. The
SPA's `copyDraftItem` (`web/src/schedules/itemRules.ts`) copies every source field + display name, so
copying a MultiCollection/SmartCollection/Rerun item preserves its source. Deliberate deviation
fixing the Blazor omission.

**(c) The shuffled-schedule GET normalization (`EnforceProperties`) is preserved lossiness, matching
Blazor.** When a schedule has `ShuffleScheduleItems`, `GET .../items` still rewrites startType→Dynamic,
Flood→One, and Playlist/Rerun playbackOrder→None (and zeroes discardToFillAttempts for non-random
Duration items). The SPA does **not** fight this — it hides the Fixed start type and Flood playout
mode from the option lists (and disables the reorder arrows) for shuffled schedules, mirroring Blazor,
so a GET→edit→PUT round-trip stays consistent with the server's read-side normalization. Issue #207.
