fix(scheduling): stop silently resetting fill-group state + invalidate deco edits (#251, #252) #258

Merged
timothy merged 2 commits from fix/251-252-scheduling-invalidation into main 2026-07-11 15:26:13 +02:00
Owner

Closes #251, closes #252. Priority pickups from the mutation-safety audit sweep (adversarial-reviewer #21/#23), both backend-only running-product correctness bugs.

#252 — schedule-items PUT no longer wipes fill-group progression

Root cause: ReplaceProgramScheduleItemsHandler deleted and re-inserted every item on every save (even a no-op PUT-back), and PlayoutScheduleItemFillGroupIndex.ProgramScheduleItemId is OnDelete(Cascade) — so every schedule save silently cascade-deleted the persisted fill-group/shuffle enumerator state for all playouts using the schedule.
Fix: positional in-place reconcile. For a same-typed slot, copy scalars via CurrentValues.SetValues (BuildItem stays the single source of item construction → no field silently dropped) and rebuild the watermark/graphics join rows, keeping the item id — and with it the fill-group index. A subtype change or surplus falls back to delete+insert for that one slot. The request DTO carries no stable item id, so position is the only key available; true content-aware stable identity is deferred to the shared concurrency/round-trip contract in #253 (per the tracker's scoping note).

#251 — deco / deco-template edits now invalidate dependent playouts

Root cause: deco/break/default-filler content is only (re)applied during a playout Reset build (a Continue keeps the frozen filler items), BlockKey change-detection has no deco dimension to self-heal, and auto-Reset is gated on DailyRebuildTime (null by default). The editors enqueued nothing (a commented-out TODO in ReplaceDecoTemplateItemsHandler), so edited filler/break content stayed stale indefinitely until a manual Reset.
Fix: enqueue BuildPlayout(Reset) for exactly the affected playouts:

  • ReplaceDecoTemplateItemsHandler → playouts via PlayoutTemplate.DecoTemplateId
  • UpdateDecoHandler → playouts via Playout.DecoId and via deco-template items referencing the deco
  • UpdateDefaultDecoHandler → the reassigned playout (adjacent same-class fix; the assignment change has the identical stale-until-Reset symptom)

Post-commit enqueues use CancellationToken.None (audit #22 policy) so a late cancellation can't drop the rebuild after the data has changed.

Tests

  • ReplaceProgramScheduleItemsReconcileTests — no-op preserves ids + fill-group state; reorder preserves the untouched item's id/state; in-place update still applies watermark edits; subtype change replaces the item.
  • DecoInvalidationTests — each of the three handlers enqueues Reset for the right playouts and not the control playout.
  • Each suite proven non-vacuous against a negative control (inverted the real primitive rather than stubbing, verified 0 CS errors so the --no-build run used a fresh dll).
  • Existing ScheduleItemResponseRoundTripTests (GET→PUT→GET fixed point across every field) stays green — the safety net that the in-place SetValues drops no field. Full ErsatzTV.Tests suite: 1067 passed.

Backend-only; no /api/* contract change, so no OpenAPI/doc regeneration.

🤖 Generated with Claude Code

Closes #251, closes #252. Priority pickups from the mutation-safety audit sweep (adversarial-reviewer #21/#23), both backend-only running-product correctness bugs. ## #252 — schedule-items PUT no longer wipes fill-group progression **Root cause:** `ReplaceProgramScheduleItemsHandler` deleted and re-inserted every item on every save (even a no-op PUT-back), and `PlayoutScheduleItemFillGroupIndex.ProgramScheduleItemId` is `OnDelete(Cascade)` — so every schedule save silently cascade-deleted the persisted fill-group/shuffle enumerator state for all playouts using the schedule. **Fix:** positional **in-place reconcile**. For a same-typed slot, copy scalars via `CurrentValues.SetValues` (`BuildItem` stays the single source of item construction → no field silently dropped) and rebuild the watermark/graphics join rows, keeping the item id — and with it the fill-group index. A subtype change or surplus falls back to delete+insert for that one slot. The request DTO carries **no stable item id**, so position is the only key available; true content-aware stable identity is deferred to the shared concurrency/round-trip contract in **#253** (per the tracker's scoping note). ## #251 — deco / deco-template edits now invalidate dependent playouts **Root cause:** deco/break/default-filler content is only (re)applied during a playout **Reset** build (a `Continue` keeps the frozen filler items), `BlockKey` change-detection has no deco dimension to self-heal, and auto-Reset is gated on `DailyRebuildTime` (null by default). The editors enqueued nothing (a commented-out TODO in `ReplaceDecoTemplateItemsHandler`), so edited filler/break content stayed stale **indefinitely** until a manual Reset. **Fix:** enqueue `BuildPlayout(Reset)` for exactly the affected playouts: - `ReplaceDecoTemplateItemsHandler` → playouts via `PlayoutTemplate.DecoTemplateId` - `UpdateDecoHandler` → playouts via `Playout.DecoId` and via deco-template items referencing the deco - `UpdateDefaultDecoHandler` → the reassigned playout (adjacent same-class fix; the assignment change has the identical stale-until-Reset symptom) Post-commit enqueues use `CancellationToken.None` (audit #22 policy) so a late cancellation can't drop the rebuild after the data has changed. ## Tests - `ReplaceProgramScheduleItemsReconcileTests` — no-op preserves ids + fill-group state; reorder preserves the untouched item's id/state; in-place update still applies watermark edits; subtype change replaces the item. - `DecoInvalidationTests` — each of the three handlers enqueues `Reset` for the right playouts and not the control playout. - Each suite proven **non-vacuous** against a negative control (inverted the real primitive rather than stubbing, verified `0 CS errors` so the `--no-build` run used a fresh dll). - Existing `ScheduleItemResponseRoundTripTests` (GET→PUT→GET fixed point across every field) stays green — the safety net that the in-place `SetValues` drops no field. Full `ErsatzTV.Tests` suite: **1067 passed**. Backend-only; no `/api/*` contract change, so no OpenAPI/doc regeneration. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-11 15:09:31 +02:00
fix(scheduling): stop silently resetting fill-group state + invalidate deco edits (#251, #252)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m56s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m22s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
64473b136d
#252: ReplaceProgramScheduleItems deleted and re-inserted every item on every
save (even a no-op PUT-back), and PlayoutScheduleItemFillGroupIndex.ProgramScheduleItemId
is OnDelete(Cascade) — so every schedule save silently wiped persisted
fill-group/shuffle enumerator progression for all playouts using the schedule.
Switch to a positional in-place reconcile: for a same-typed slot, copy scalars via
CurrentValues.SetValues (BuildItem stays the single source of item construction, so
no field is dropped) and rebuild the watermark/graphics join rows, keeping the item
id — and with it the fill-group index. Subtype change / surplus falls back to
delete+insert for that slot only. The request DTO carries no stable item id, so
position is the only key here; true content-aware stable identity is deferred to the
shared concurrency/round-trip contract in #253.

#251: deco / deco-template CONTENT edits (and default-deco assignment) only take
effect on a playout Reset build — deco/break/default-filler content is applied during
Reset, a Continue keeps the frozen filler items, and BlockKey change-detection has no
deco dimension to self-heal. The editors enqueued nothing (a commented-out TODO in
ReplaceDecoTemplateItemsHandler), so filler/break content stayed stale indefinitely
until a manual Reset. Enqueue BuildPlayout(Reset) for exactly the affected playouts:
- ReplaceDecoTemplateItemsHandler: playouts via PlayoutTemplate.DecoTemplateId
- UpdateDecoHandler: playouts via Playout.DecoId and via deco-template items
- UpdateDefaultDecoHandler: the reassigned playout (adjacent same-class fix)
Post-commit enqueues use CancellationToken.None (audit #22 policy).

Tests: DecoInvalidationTests + ReplaceProgramScheduleItemsReconcileTests, each proven
non-vacuous against a negative control (inverted the primitive, verified 0 CS errors so
the --no-build run used a fresh dll). Full ErsatzTV.Tests suite green (1067).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy added 1 commit 2026-07-11 15:18:40 +02:00
fix(review): close post-commit cancellation window + guard default-deco enqueue (Codex Medium/Low)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m55s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
51ac11e5aa
Independent Codex review of the fix diff surfaced two real findings the fork pass missed:

- Medium: the #251 affected-playout QUERIES in ReplaceDecoTemplateItemsHandler and
  UpdateDecoHandler still ran on the request `cancellationToken`, so a cancellation
  landing after SaveChanges committed but before those queries executed would throw
  before the CancellationToken.None enqueue — the edit committed but no playout Reset,
  re-opening the stale-content bug in that window. Run the entire post-commit
  invalidation (queries + enqueue) on CancellationToken.None so the side effect can't
  be half-aborted once the data has changed.
- Low: UpdateDefaultDecoHandler enqueued a Reset for request.PlayoutId even when
  ExecuteUpdateAsync matched 0 rows (nonexistent playout), creating a background build
  request for an id that isn't there. Guard the enqueue on rows-updated > 0 so the
  enqueued set equals the affected set. Added a regression test.

Also corrected the ReplaceProgramScheduleItemsHandler comments: the schedule-item
hierarchy is TPT (table-per-type), not TPH — the SetValues reconcile is safe either way
(same-runtime-type guard; no discriminator to corrupt), Codex confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy merged commit 87060aa3de into main 2026-07-11 15:26:13 +02:00
timothy deleted branch fix/251-252-scheduling-invalidation 2026-07-11 15:26:13 +02:00
Sign in to join this conversation.