Commit Graph
7 Commits
Author SHA1 Message Date
timothyandClaude Opus 4.8 d2d678aae8 test/docs(404): add unrelated-edit round-trip test; correct MultiCollection-only rationale
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m20s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m10s
Review + live-E2E follow-ups (no behavior change):
- Add a test proving the canonical silent-reset trap directly: a rename-only save
  round-trips both weights untouched (the prior test only edited the weight it
  asserted). Cold review nit.
- Correct the rationale in itemRules.ts + decisions.md: WeightedShuffle is
  MultiCollection-only in the SPA for *meaningfulness* (per-source weights need
  2+ sources), NOT because the classic write path rejects it — live-E2E confirmed
  the classic engine ACCEPTS it on a plain Collection (200) and degrades to
  fair-share. The rejection is on the separate playlist/block write paths, whose
  editors keep their own order lists and already omit it.

Live-E2E (real API): weighted multi-collection create + read round-trips weights;
rename-only PUT preserves them (no silent reset); WeightedShuffle persists on a
classic MultiCollection schedule item. Ratio itself is pinned by the existing
PlayoutBuildGoldenTests.Classic_weighted (3:1), untouched by this SPA change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 11:39:29 +02:00
timothyandClaude Opus 4.8 d639946b5c feat(404): weighted-distribution SPA — per-source weight inputs + WeightedShuffle order
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m39s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m37s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 24m50s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
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>
2026-07-19 11:28:12 +02:00
timothyandClaude Opus 4.8 b7428a9b95 feat(#259): SPA round-trips schedule item server id
The backend (already on this branch) added an optional int? Id to
ScheduleItemRequest so the server reconciles PUT /api/schedules/{id}/items
rows by identity instead of by array position. The SPA previously
discarded the server id on load (only a client-local _key survived) and
never sent one back, so a reorder could misattribute fill-group/shuffle
state onto the wrong persisted row.

- itemRules.ts: fromResponse now captures the response item's id onto
  the draft; normalizeForSave emits it back unchanged. newDraftItem and
  copyDraftItem explicitly set id: null (a brand-new/copied row was
  never persisted under an id, and copyDraftItem must not duplicate the
  source's id onto a second row).
- scheduleItem.ts (Add-to-schedule dialog, POST path): id: null for the
  same reason — it always creates a new row.
- SchedulesScreen.tsx save(): the PUT-response re-seed already existed
  (fromResponse over the response array) but now carries ids through.
  This matters because a subtype/playout-mode switch can be a
  delete+insert server-side, so the response id for that row can differ
  from what was submitted — a second save must use the *response's* id
  or the server 422s it as unknown. Added a comment documenting this.
- schedules.ts: replaced the stale "server reuses same-typed rows by
  position" comment with the current id-based reconcile contract.
- Added/updated tests in itemRules.test.ts, SchedulesScreen.test.tsx,
  and AddToScheduleDialog.test.tsx covering id round-tripping, the
  null-id-for-new/copied-item cases, and a second-save-reuses-the-
  response-id regression test.

Verified: npm run lint, tsc -b --noEmit, npm run build, and npm test
(680/680) all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:51:24 +02:00
timothyandClaude Fable 5 4bc43893c1 fix(spa): guard schedule editor against destructive cross-save + lost updates (F1/F3/F4/F5)
F1: switching schedules synchronously clears items/baseline/selection/dirty
before setActiveId (applySwitch) and gates every mutation surface on a
successful items load for the CURRENT activeId (itemsLoaded) — a failed items
GET for schedule B can no longer leave B's header over A's dirty draft and PUT
A's lineup into B.
F3: key={selectedItem._key} on ScheduleItemInspector so per-item child state
(PlaylistPicker groupId, SearchPicker query) resets on selection change.
F4: mutate() no-ops and all edit surfaces disable while saving, so edits during
an in-flight Save can't be silently discarded by the Save .then.
F5: create-schedule auto-switch routes through guardedSwitch so a dirty draft
gets the same discard confirm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:00:34 +02:00
timothyandClaude Fable 5 8feb9a1367 fix(spa): reconcile playbackOrder + multipleMode on schedule collection-type change (F2)
After a collection-type change, snap playbackOrder to the new type's first
offered order when the current one is no longer valid (e.g. Collection+Marathon
-> TelevisionShow left a stale 'Marathon' while the native <select> displayed
'Chronological' and Marathon fields stayed visible), and reconcile multipleMode
into the valid set for the new (type, order) state (e.g. CollectionSize
surviving a switch into Playlist). Replaces the narrow MultiCollection/Playlist
special-cases with a general invariant. Adds repro + a from/to-pair invariant
test; updates the Playlist multipleMode test to the corrected behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:00:34 +02:00
timothyandClaude Fable 5 2687d33637 feat(spa): rebuild schedules editor to full mutation parity (#207)
Extract ScheduleScreen from App.tsx into screens/SchedulesScreen.tsx +
schedules/ domain folder (itemRules, pickers, inspector, ScheduleForm).
Draft model with explicit Save (single destructive PUT), Discard, dirty
guard (navigationGuard + beforeunload), schedule CRUD, and all Blazor
item fields/gates/resets. Rewrite api/schedules.ts to the flat DTO +
CRUD + languages/filler-by-kind pickers. Live TopBar Add Schedule via a
window CustomEvent. Screen + nav-guard tests; App.test updated for the
extracted screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:32:25 +02:00
timothyandClaude Fable 5 5816c31136 feat(spa): pure schedule-item rules module (#207)
Blazor-parity gating, option lists and forced-reset transforms for the
schedules editor, exhaustively unit-tested (44 cases). No React/fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:09:49 +02:00