Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review (fork + Codex, both flagged) of PR #241: - SPA (both reviewers): removing PENDING_GRACE_TICKS wholesale reintroduced a stuck scan button. A 202'd scan that finishes between 10s polls (short/empty library) is never observed active, so its optimistic pending flag wedged the button disabled until reload. Restore a BOUNDED grace net (pruneGraceExpiredPending) — re-scoped honestly: it absorbs the inherent queue->observed-active lag and the fast-completion race, NOT the removed lying-200 compensation (the POST now returns 409/404/422 honestly). Bounds pending to PENDING_GRACE_TICKS * pollMs (~30s). - SPA 409 (Codex): on "already scanning" the button was cleared+reconciled, but a scan-status still lagging the in-progress scan re-enabled the button and let the user fire repeated 409s. Keep the pending flag on 409 (no toast) so the button stays disabled; polling promotes or expires it. - Scheduler (Codex): the Plex-Shows tail-token batch and the local/Jellyfin/Emby scan enqueues had no compensating unlock — a WriteAsync failure after LockLibrary (cancellation on shutdown) stranded the library lock. Wrap each acquired-lock enqueue in try/catch → UnlockLibrary → rethrow (the Plex catch covers both writes, since the library message carries Unlock: false and the un-enqueued networks message was the sole releaser). Tests: two new App.test.tsx cases — grace-window expiry re-enables the button, and 409 keeps it disabled through the queue->active lag. Ref #232. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>