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.
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)
#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>
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 main2026-07-11 15:26:13 +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.
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:
ReplaceProgramScheduleItemsHandlerdeleted and re-inserted every item on every save (even a no-op PUT-back), andPlayoutScheduleItemFillGroupIndex.ProgramScheduleItemIdisOnDelete(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(BuildItemstays 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
Continuekeeps the frozen filler items),BlockKeychange-detection has no deco dimension to self-heal, and auto-Reset is gated onDailyRebuildTime(null by default). The editors enqueued nothing (a commented-out TODO inReplaceDecoTemplateItemsHandler), so edited filler/break content stayed stale indefinitely until a manual Reset.Fix: enqueue
BuildPlayout(Reset)for exactly the affected playouts:ReplaceDecoTemplateItemsHandler→ playouts viaPlayoutTemplate.DecoTemplateIdUpdateDecoHandler→ playouts viaPlayout.DecoIdand via deco-template items referencing the decoUpdateDefaultDecoHandler→ 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 enqueuesResetfor the right playouts and not the control playout.0 CS errorsso the--no-buildrun used a fresh dll).ScheduleItemResponseRoundTripTests(GET→PUT→GET fixed point across every field) stays green — the safety net that the in-placeSetValuesdrops no field. FullErsatzTV.Testssuite: 1067 passed.Backend-only; no
/api/*contract change, so no OpenAPI/doc regeneration.🤖 Generated with Claude Code