fix(api): gate playout mutations on EntityLocker build lock (409) + SPA lock mirror (#215) #225

Closed
timothy wants to merge 0 commits from fix/215-entitylocker into main
Owner

Closes #215 (adversarial-reviewer#18 promoted removal gate: Blazor deletion would erase the EntityLocker safety invariant).

  • All id-keyed playout mutations (4 PUTs, 2 erases, DELETE) + channel-level reset now return 409 ProblemDetails while IsPlayoutLocked — new shared ApiResults.ConflictProblem (mirrors NotFoundProblem; Trakt precedent left as-is). reset-all unchanged: its handler already skips locked playouts.
  • PlayoutListItemResponseModel.IsLocked stamped from the locker; SPA disables Reset/Erase/Delete on locked rows, shows a Building… badge, and refreshes on 409. No new polling.
  • OpenAPI + TS regenerated; contract + metadata tests extended with 409 rows; controller guard tests with substituted IEntityLocker.
  • docs: api-conventions §3a (409/ConflictProblem convention), blazor-route-parity playouts verdict, decisions.md 2026-07-10 entry.

ErsatzTV.Tests 1003/1003; web tsc + 478/478 + lint + build + check:api clean.

🤖 Generated with Claude Code

Closes #215 (adversarial-reviewer#18 promoted removal gate: Blazor deletion would erase the EntityLocker safety invariant). - All id-keyed playout mutations (4 PUTs, 2 erases, DELETE) + channel-level reset now return 409 ProblemDetails while `IsPlayoutLocked` — new shared `ApiResults.ConflictProblem` (mirrors NotFoundProblem; Trakt precedent left as-is). `reset-all` unchanged: its handler already skips locked playouts. - `PlayoutListItemResponseModel.IsLocked` stamped from the locker; SPA disables Reset/Erase/Delete on locked rows, shows a Building… badge, and refreshes on 409. No new polling. - OpenAPI + TS regenerated; contract + metadata tests extended with 409 rows; controller guard tests with substituted IEntityLocker. - docs: api-conventions §3a (409/ConflictProblem convention), blazor-route-parity playouts verdict, decisions.md 2026-07-10 entry. ErsatzTV.Tests 1003/1003; web tsc + 478/478 + lint + build + check:api clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-10 23:35:11 +02:00
fix(api): gate playout mutations on EntityLocker build lock (409) + mirror lock state in SPA (fixes #215)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
28910ff557
Blazor disabled per-playout Reset/Erase/Delete/Edit while a BuildPlayout was
in flight (EntityLocker.IsPlayoutLocked); the REST API had no equivalent, so a
client could race an in-flight build with a destructive ExecuteDelete and leave
a half-built playout. After Blazor removal this safety invariant would vanish
entirely (adversarial-reviewer#18 removal gate).

Server:
- Add public ApiResults.ConflictProblem(title, detail) (409, mirrors NotFoundProblem).
- Inject IEntityLocker into PlayoutController; guard every id-keyed mutation
  (PUT {id}, PUT .../deco, PUT .../alternate-schedules, PUT .../templates,
  POST .../erase-items, POST .../erase-items-and-history, DELETE {id}) → 409
  when IsPlayoutLocked(id); add [ProducesResponseType(...409)] to each.
- Guard ChannelController.ResetPlayout the same way after resolving the id.
- reset-all stays 202 (ResetAllPlayoutsHandler already skips locked playouts).
- Stamp IsLocked onto PlayoutListItemResponseModel from IsPlayoutLocked.

SPA:
- Disable Reset/Erase/Erase-and-history/Delete for a locked row + show a
  "Building…" Badge; on a 409 surface the error and refresh the list.

Tests: controller-level 409 guard tests (delete/erase/PUT/deco/channel-reset)
+ IsLocked projection test; new OpenAPI contract + metadata 409 rows.
Docs: api-conventions §3a, blazor-route-parity playouts verdict, decisions.md.
Regenerated v1.json + web types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy added 1 commit 2026-07-10 23:38:51 +02:00
docs(review): state check-then-act (advisory) semantics of the 409 build-lock guard honestly (#215)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m56s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m56s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
dbc47f4cae
Adversarial review finding 1 on PR #225: decisions.md and api-conventions §3a
read as if the race were eliminated; the guard only narrows it (a queued build
can acquire the lock after the check passes). Also records why true lock
acquisition per mutation was not taken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy added 1 commit 2026-07-11 00:31:11 +02:00
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m51s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
83455fa42e
Author
Owner

Merged to main via integration PR #239 (all six gate-batch branches integrated in one CI round after pairwise doc conflicts; this PR's commits are reachable from main — see the merge commits there). Closing manually since Gitea did not auto-detect the branch merge.

Merged to main via integration PR #239 (all six gate-batch branches integrated in one CI round after pairwise doc conflicts; this PR's commits are reachable from main — see the merge commits there). Closing manually since Gitea did not auto-detect the branch merge.
timothy closed this pull request 2026-07-11 11:37:51 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.