Commit Graph
2644 Commits
Author SHA1 Message Date
timothyandClaude Opus 4.8 8a2238b62e fix(ui): pattern-based legacy→SPA redirect matcher for parameterized routes (#204)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m28s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extend LegacyUiRedirects from an exact-match dictionary to a two-tier matcher:
Tier 1 keeps the exact Map (now 52 entries incl. the ?kind= browse roots),
Tier 2 adds 36 ordered segment-template PatternRules for id-carrying routes.
{id} is a strict positive integer (non-int/0/neg/overflow falls through), which
also makes the rule set collision-free by construction. New AppendQueryString
helper merges the incoming query into ?kind= targets with '&' (kills the
double-'?' bug); one-line Startup change keeps the redirect GET/HEAD-only 302
before UseRouting.

Completes phase-(a) Step 1 for every PARITY-OK route (#91 phase b); the
catch-all fallback replacing MapFallbackToPage stays with the removal PR.
/media/sources/* (#202) and /system/health remain deliberately un-redirected.

fixes #204

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:39:56 +02:00
timothyandClaude Opus 4.8 06c877b5fc docs(handoff): lore — post-commit invalidation on None + SetValues positional reconcile (#251, #252)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m30s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m15s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m30s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:28:29 +02:00
timothy 87060aa3de Merge pull request 'fix(scheduling): stop silently resetting fill-group state + invalidate deco edits (#251, #252)' (#258) from fix/251-252-scheduling-invalidation into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-11 13:26:12 +00:00
timothyandClaude Opus 4.8 51ac11e5aa 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
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>
2026-07-11 15:18:38 +02:00
timothyandClaude Opus 4.8 64473b136d 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
#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>
2026-07-11 15:08:58 +02:00
timothyandClaude Opus 4.8 d93bca653a docs(handoff): lore — negative-control a flag fix by inverting, not if(true) (CS0219 stale-dll trap)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m57s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m21s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m41s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:15:06 +02:00
timothy 77272ed5a7 Merge pull request 'fix(locking): release troubleshooting + playout locks on all terminal paths (#233, #234)' (#250) from fix/233-234-lock-leaks into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-11 12:12:52 +00:00
timothyandClaude Opus 4.8 8cdc963636 fix(review): centralize troubleshooting lock ownership in Handle (Codex High)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m3s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m4s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent Codex review of PR #250 caught a cross-release the fork missed: the
outer catch in PrepareTroubleshootingPlaybackHandler released the troubleshooting
lock unconditionally, so an exception BEFORE this caller acquired it (e.g. request
cancellation during validation, or a DB error) would release a lock held by another
session.

Fix: track ownership with a Handle-scoped `lockAcquired` flag and gate the catch on
it. Acquisition for the media-item path moves out of GetProcess up into Handle (after
validation succeeds), so one place owns the full lifecycle: acquire -> on Left release
-> on success hand off to StartTroubleshootingPlayback -> on any exception release only
if we own it. GetProcess is now lock-free.

Test: Handle_Should_Not_Release_Lock_It_Never_Acquired (exception before acquisition ->
no Unlock, no cross-release), verified non-vacuous against an inverted-condition
negative control. Strengthened the empty-path test to also assert acquisition happened.

Deferred (noted for close comment): worker-dispatch-failure lock leak and
BuildPlayout silent-skip observability are pre-existing / sanctioned -> #235.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:02:03 +02:00
timothyandClaude Opus 4.8 084d4c4ca1 fix(locking): release troubleshooting + playout locks on all terminal paths (#233, #234)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m55s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m24s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Consume the EntityLocker ownership contract (#231/#241: Lock* returns true iff
this caller won the slot) at three lock-leak sites surfaced by adversarial-reviewer#20.

#233 (F3) — troubleshooting playback:
- PrepareTroubleshootingPlaybackHandler: both lock sites now acquire via
  `if (!LockTroubleshootingPlayback())` (kills the check-then-set TOCTOU) and the
  empty-media-path Left return releases the lock it acquired — previously it leaked,
  wedging the status endpoint at "running" forever for a file gone from disk.
- TroubleshootController.TroubleshootPlayback: lock conflict is now 409 ProblemDetails
  (was a bare 404, indistinguishable from a bad id); the Prepare-success -> enqueue
  window releases the lock if we never hand off to StartTroubleshootingPlayback.

#234 (F4 + F5.2) — playout builds:
- ExtractEmbeddedSubtitlesHandler: try/finally releases exactly the playouts it
  locked, on every terminal path (cancellation early-return, swallowed cancellation,
  any exception) — no more permanent leaks after cancelled mid-extraction, and no
  cross-release of playouts held by someone else.
- BuildPlayoutHandler: skips (logs, returns Right) when LockPlayout returns false
  instead of building unlocked and cross-releasing the other owner's lock in finally.

Tests: handler-level release-discipline tests (Prepare empty-path, Extract
cancellation + no-cross-release, BuildPlayout skip + finally-release) via the
InMemoryTvContext harness, a TroubleshootController 409 test, and OpenApi contract
cases for the m3u8 endpoint's 409. OpenAPI regenerated. All non-vacuous (F3 verified
against a negative control).

Fixes #233, #234

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 13:44:47 +02:00
timothy 533c6c1e9f Merge pull request 'refactor(spa): extract ChannelsScreen from App.tsx (#244, epic #243 phase 1)' (#249) from feat/244-channels-extract into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m55s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m1s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m30s
2026-07-11 11:38:47 +00:00
timothyandClaude Opus 4.8 f8390bb008 refactor(spa): extract ChannelsScreen from App.tsx (#244, epic #243 phase 1)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m57s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Move the Channels domain verbatim out of web/src/App.tsx into
web/src/screens/ChannelsScreen.tsx (zero-prop, self-sufficient, mirroring the
SchedulesScreen extraction). Pure structural move: no API, route, CSS, or
visual change. App.tsx retains only the import + the <ChannelsScreen /> dispatch.

- 14 symbols moved (ChannelViewFilter → ChannelTableRow); the Dashboard-owned
  progressFromNowPlaying is inlined into the moved progressFromChannelState so
  the screen has no import back into App.tsx (behavior-identical).
- 12 Channels behavior tests moved to a colocated ChannelsScreen.test.tsx with
  its own scoped fetch mock (renders <ChannelsScreen /> directly, no
  mockDashboardApi); App.test.tsx keeps one nav-smoke test for the route.
- Pruned 12 now-dead App.tsx imports; shared symbols (ChannelState,
  messageFromError, ApiError, useChannelsQuery) verified still used and kept.
- Docs: spa-conventions §6 (extracted-screen own-fetch-mock convention),
  decisions.md (single-file rationale; no web/src/channels/ sibling dir, unlike
  Schedules; inlined helper; #238 deferral).

Verified: web vitest 587 passed, eslint clean, tsc/vite build clean,
check:api no drift. #212 empty-lineup bare-create success+failure coverage
preserved. #238 TopBar dead-button left as-is (its owned bug; shell redesign
is epic phase 4 / #247).

refs #244 #243

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 13:27:50 +02:00
timothyandClaude Fable 5 ea0eb0ab76 docs(handoff): lessons from #242 — re-review the fix commit before closing; disabled={saving} SchedulesScreen convention
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m48s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m32s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m32s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:16:39 +02:00
timothy eba22ce679 Merge pull request 'fix: reviewer post-merge blockers — empty-lineup bare create (#212) + dirty-guard popstate/shuffle-toggle (#230)' (#242) from fix/212-230-review-blockers into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-11 11:13:55 +00:00
timothyandClaude Opus 4.8 490b6d2ef0 docs(handoff): lore — concurrency tests need negative control + stress loop; audit workaround purpose before deleting
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 13:07:51 +02:00
timothyandClaude Fable 5 b26045676b fix(spa): gate Delete on saving to close the last switch-during-save clobber
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Codex re-review of the prior fix commit found the Edit gate closed the exact
repro but two paths remained. One is reachable: Delete was the only
schedule-switch path that guardedSwitch's `saving` guard didn't cover — deleting
mid-save runs applySwitch to the next schedule while the in-flight items PUT is
still outstanding, and that PUT's completion handler then overwrites the next
schedule's draft with the deleted schedule's response. Delete is now
`disabled={saving}`, consistent with the Select, Edit, and guardedSwitch.

Regression: the deferred-PUT test now also asserts Delete is disabled in-flight
and re-enables after the save settles.

Also widens the test mock's onRequest return type to `Response | Promise<Response>
| null` (removes the `as unknown as Response` cast — a test-only type hole the
re-review flagged).

Deferred to #248: the other residual path (properties dialog not focus-trapped, so
keyboard focus can escape to underlying Add/Save mid-save) is a pre-existing,
cross-cutting overlay.tsx a11y gap affecting all dialogs — out of scope for this
targeted blocker fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:06:03 +02:00
timothy c42928e891 Merge pull request 'fix(locking): EntityLocker atomicity + scan-lifecycle honesty (#231, #232)' (#241) from batch/231-232-lock-soundness into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-11 11:05:16 +00:00
timothyandClaude Fable 5 6f6fcf6c63 fix(spa): review follow-ups — surface empty-state create error, gate Edit while saving
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Two findings from the PR #242 adversarial review (Codex + Claude fork):

1. ChannelsEmptyState swallowed mutationError: on the fresh-install path #212
   targets, a bare-create can 4xx (e.g. no default ffmpeg profile), but the empty
   branch never rendered the error alert the non-empty screen shows — the user saw
   only a spinner re-enable. The empty state now renders the same ctv-channels-error
   alert. Regression: App.test.tsx asserts the error surfaces + no navigation on an
   empty lineup.

2. SchedulesScreen Edit button was not gated on `saving` (Codex): during an items
   save PUT the draft is still dirty, so a discard-to-open → shuffle-flip could let
   the in-flight PUT resolve AFTER the shuffle reload and clobber the normalized
   draft with the pre-shuffle body. Edit is now disabled while saving (consistent
   with the schedule Select). Regression: a deferred PUT proves Edit is disabled
   in-flight and re-enables once the save settles.

Deferred as nits (both reviews rate low): vetoed navigateToPath leaves two stray
history entries (cosmetic, rare — fixing means refactoring central nav); rapid
double-Back is best-effort (inherent popstate non-cancellability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:55:07 +02:00
timothyandClaude Opus 4.8 702b0121cd fix(review): bounded scan-pending net + scheduler compensating unlock (#232)
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>
2026-07-11 12:54:48 +02:00
timothy 78e4a3c738 Merge branch 'slice/232-scan' into batch/231-232-lock-soundness
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 12:39:04 +02:00
timothyandClaude Fable 5 11b43d5e06 fix(spa): reload item draft when a save flips shuffleScheduleItems (#230 finding 2)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Editing a schedule's properties to change shuffleScheduleItems for the active
schedule left the open items draft stale: hidden Fixed/Flood start values could
be saved back and the inspector kept offering start-type controls the schedule
no longer supports. onScheduleSaved now detects a shuffle flip on the active
schedule and reloads the items via GET so the server's EnforceProperties
re-normalizes the draft. The schedule-level flags already refresh from the save
response (setBoot maps `saved` into the list).

Decision: chose "block opening the edit dialog while the item draft is dirty"
(confirm-to-discard, guardedSwitch semantics) over confirm-at-reload — the
smaller fully-consistent change. It guarantees the properties editor only ever
opens over a clean baseline draft, so the post-save reload is lossless and
avoids the awkward state where a cancelled discard leaves a now-shuffled
schedule holding Fixed values.

Regression: SchedulesScreen.test.tsx — Fixed item on a non-shuffled schedule →
edit properties to shuffle=true → a fresh items GET fires, the Fixed option is
gone, and a subsequent Save's PUT carries no Fixed startType.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:38:30 +02:00
timothyandClaude Fable 5 f09045e135 fix(spa): dirty guard covers browser Back/Forward (popstate) (#230 finding 1)
canLeaveCurrentScreen() was only consulted in App's navigate() (sidebar/nav
clicks); browser Back/Forward switched screens unguarded. A popstate can't be
cancelled, so App's popstate handler now, on a vetoed guard, re-pushes the
pre-pop path (tracked in currentPathRef, updated on every approved navigation)
and leaves activeRoute untouched — undoing the browser's URL change. The same
handler covers the synthetic pop navigateToPath() dispatches. Re-pushing is
safe: only one screen is mounted at a time and the guard-registering screen
(schedules) owns no internal popstate listener, so no sub-path screen's
pathname state can desync. Effect cleanup keeps StrictMode double-mount from
double-registering.

Docs: spa-conventions §8 rewritten from navigate-only to describe popstate
coverage. Regression: App.test.tsx dirties the schedules draft, simulates
popstate → confirm called; cancel keeps route + re-pushes path; accept
switches route and unmounts the draft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:38:19 +02:00
timothyandClaude Fable 5 d3d50f60c4 fix(spa): bare-channel create reachable on an empty lineup (#212 reopened)
ChannelsScreen returned ChannelsEmptyState before the action bar that owns
"New blank channel", so a fresh install could never create its first channel.
The empty state now offers both create paths (bare-create + ChannelBuilder),
reusing the exact createBlankChannel handler (number = max+1 → 1 on empty,
group "ErsatzTV", default ffmpeg profile).

Regression: App.test.tsx bare-creates from a [] lineup, asserts the POST
payload (number "1") + navigation to /app/edit-channel/{id}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:38:10 +02:00
timothyandClaude Opus 4.8 088644e8c0 feat(libraries): honest scan lifecycle + one-lock-one-release discipline (#232)
Scan queue handler now returns a QueueLibraryScanResult enum
(Queued|NotFound|SyncDisabled|AlreadyScanning) instead of a lying bool;
LibrariesController.ScanLibrary maps them to 202/404/422/409 with ProblemDetails.
Guard the lock->enqueue with the EnqueueWithTraktLock compensating-unlock pattern.

ScannerService now releases every library/collection lock in a finally so a handler
exception can't leak the lock. Plex "Shows" scheduler batch (one lock, two messages)
now has only the trailing SynchronizePlexNetworks carry the single release
(Unlock flag), mirroring the scheduler Trakt tail-token precedent.

Guard the other lock->enqueue producers (Create/UpdateLocalLibrary, UpdateTraktList)
with compensating unlock. SPA drops the PENDING_GRACE_TICKS heuristic now that the
POST reports 202/409/404/422 directly: 202 -> pending+poll, 409 -> reconcile (no
error toast), 404/422 -> surface error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:38:08 +02:00
timothyandClaude Opus 4.8 275908ec11 fix(locking): atomic EntityLocker flags + single-owner release contract (#231)
The six plain-bool lock flags (Plex, Trakt, Emby/Jellyfin/Plex collections,
troubleshooting playback) used a non-atomic check-then-set, so two concurrent
Lock* callers could both win. Convert them to int flags mutated only via
Interlocked.CompareExchange, so the caller that wins the 0->1 transition is the
sole owner and the only one that fires the change event. The three
ConcurrentDictionary-backed kinds (Library/Playout/RemoteMediaSource) were
already atomic; drop their redundant ContainsKey pre-checks.

Define the ownership contract (tokenless single-owner discipline, no interface
change) on IEntityLocker and in docs/decisions.md: a true from Lock* confers
ownership of exactly one release; Unlock* on an unlocked slot returns false,
fires no event, and logs a warning (the double-release / non-owner tripwire).

Adds EntityLockerTests (real locker, parallel-caller races) proving exactly one
winner per kind, one-releaser-per-slot, and event-fires-once-per-transition.

Ref #231. Scan-lifecycle call-site fixes that consume this contract land in the
same PR (#232); the BuildPlayout/subtitle finally-gating is #234.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:30:36 +02:00
timothyandClaude Fable 5 554f658989 docs(handoff): orchestrator is Opus-by-default with explicit Fable escalation triggers
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 7m25s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m38s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:11:12 +02:00
timothyandClaude Fable 5 dd9230ac7d docs(handoff): lessons from #207/#212 session (lazy-Map masking, merge-regen rule, CI timeouts)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:09:19 +02:00
timothy 6bbf669403 Merge pull request 'feat: schedules editor full mutation depth + channel-editor gaps (#207, #212, #126)' (#229) from feat/207-212-schedules-channel-depth into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
2026-07-11 10:05:53 +00:00
timothy c4b7babbe8 Merge pull request 'test(api): locked-path 409 guard tests for ReplaceAlternateSchedules + ReplaceTemplates (#215)' (#240) from fix/215-guard-tests into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m4s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-11 09:59:43 +00:00
timothyandClaude Fable 5 3c7ed5c2f8 test(spa): raise timeout on trash see-all paging test (CI VM flake, run 686)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 10m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:51:11 +02:00
timothyandClaude Fable 5 67547d998a test(api): locked-path 409 guard tests for ReplaceAlternateSchedules + ReplaceTemplates (fixes #215)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Closes the reviewer-verification gap from adversarial-reviewer#18: the
locked-path 409 guard tests already existed for Delete, EraseItems,
EraseItemsAndHistory, Update, and UpdateDefaultDeco, but were missing for
PlayoutController.ReplaceAlternateSchedules and .ReplaceTemplates even
though their production guards (entityLocker.IsPlayoutLocked) were in
place. Added two tests mirroring the existing pattern exactly (409
ConflictObjectResult + ProblemDetails + DidNotReceive() on the mediator
command). ApiErrorResponseMetadataTests and OpenApiErrorResponseContractTests
already carried 409 rows for both PUT endpoints, so no changes were needed
there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:49:40 +02:00
timothyandClaude Fable 5 9467a373b8 docs(handoff): lore — Codex back for independent review passes when quota allows
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m34s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:46:10 +02:00
timothyandClaude Fable 5 b82ae0c906 merge: origin/main (review-gates batch #222/#239) into feat/207-212
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:44:58 +02:00
timothyandClaude Fable 5 5e2b428876 docs(handoff): kickoff prompt also surfaces pending adversarial-reviewer audits as pickup candidates
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:40:48 +02:00
timothyandClaude Fable 5 1bef9c9076 ci: small-runner lane + per-ref concurrency (queue starvation fix)
- build & docs-reminder jobs -> runs-on: small (dedicated small-jobs runner,
  server-management#574). Gitea dispatches a job as a runner task even when
  its 'if' skips it; the PR-run skip of 'Build & push image' waited up to
  31 min for an ubuntu-latest slot, stalling every PR run.
- concurrency scoped per event+ref with cancel-in-progress for PRs: runs
  parallelize across PRs, superseded syncs auto-cancel. Previously one global
  group serialized ALL runs (single-runner relic). Main/tag builds still
  serialize within their ref; don't push main + v* tag simultaneously
  (shared :buildcache / smoke container) — tag after main is green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:40:30 +02:00
timothy c77ed43585 Merge pull request 'Integration: review-gates batch — #220/#215/#217/#219/#218 + #213 remainder (PRs #223–#228)' (#239) from integrate/review-gates into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 8m54s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m38s
2026-07-11 00:03:46 +00:00
timothyandClaude Fable 5 f0ada7e993 docs: sweep stale #213 gap-issue cells after integration (all #213 items closed)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:53:06 +02:00
timothy 6dc9c12d8c Merge remote-tracking branch 'origin/fix/213-logs-trash' into integrate/review-gates
# Conflicts:
#	docs/blazor-route-parity.md
2026-07-11 01:49:50 +02:00
timothy cf81f28118 Merge remote-tracking branch 'origin/fix/215-entitylocker' into integrate/review-gates
# Conflicts:
#	docs/blazor-route-parity.md
#	docs/decisions.md
2026-07-11 01:49:16 +02:00
timothy aee96a1788 Merge remote-tracking branch 'origin/fix/220-episode-nav' into integrate/review-gates
# Conflicts:
#	docs/blazor-route-parity.md
2026-07-11 01:48:48 +02:00
timothy ef4b64d57c Merge remote-tracking branch 'origin/fix/218-playouts-badge' into integrate/review-gates 2026-07-11 01:48:20 +02:00
timothy c846593aa3 Merge remote-tracking branch 'origin/fix/217-219-api-fixes' into integrate/review-gates 2026-07-11 01:48:20 +02:00
timothy a039d5230c Merge remote-tracking branch 'origin/fix/213-spa-nits' into integrate/review-gates 2026-07-11 01:48:19 +02:00
timothy 245b1a5889 Merge pull request 'fix(spa): gate mutation controls on stale result sets during refetch (#221)' (#222) from fix/221-stale-refetch into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 7m46s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
2026-07-10 23:47:36 +00:00
timothyandClaude Fable 5 e5189f2921 docs(handoff): RAM-based fan-out gating (user calibration) + 2026-07-11 lessons (CI port fix, dual runners, cross-session plumbing merge)
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:42:10 +02:00
timothy 7f248fd553 Merge branch 'feat/207-212-schedules-channel-depth' of ssh://192.168.1.95:22/timothy/ersatztv into feat/207-212-schedules-channel-depth
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 01:27:39 +02:00
timothyandClaude Fable 5 3d35aa5348 fix(spa): defensively sort schedule items by index on ingest (#229)
Bug 2 (client side): array position becomes the persisted index on the next
PUT-replace, so the schedules editor must ingest strictly by the server-provided
`index` rather than trusting response row order — otherwise a reload + re-save could
silently reshuffle the lineup. Applied at both ingest points (GET load and the
replace response). Pinned by a shuffled-response-order test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:26:25 +02:00
timothyandClaude Fable 5 794bb0d4bb fix(schedules): reload write-path response with read includes + order GET by index (#229)
Bug 1 (500 on watermark/graphics save): Replace/Add handlers projected the
freshly-built entity graph, whose ProgramScheduleItemWatermark / -GraphicsElement
join rows carry only foreign-key ids — the Watermark/GraphicsElement navs are null,
and Mapper.ProjectToViewModel dereferences them unguarded, throwing an NRE that the
controller surfaced as a 500 on PUT/POST. Both handlers now reload the persisted
item(s) through the read-side include chain before projecting. Extracted that chain
into ProgramScheduleItemQueryExtensions.IncludeScheduleItemDetails() so GET, Replace
and Add share one source of truth.

Masking: PersistItems returned a lazy LanguageExt Map, and the existing round-trip
test only checked .IsRight — never enumerating it, so the deferred NRE never fired.
The new ScheduleItemWriteProjectionTests force enumeration (as the controller's
.ToList()/serialization does) and seed watermark/graphics via a separate context so
the handler's fresh factory context has nothing pre-tracked.

Bug 2 (server side): GetProgramScheduleItemsHandler now .OrderBy(i => i.Index) —
it previously returned id order, which is not index order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:26:25 +02:00
timothyandClaude Fable 5 dfd2134cb4 docs: queue protocol v2 — state moves to pinned Gitea tracker #237 (parallel-session safe)
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Handoff file reduced to static kickoff prompt + append-only lessons lore;
queue/arc/session log live in pinned tracker ersatztv#237 with in-progress
claim labels and end-of-session triage. Decision recorded in decisions.md;
docs index updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:17:26 +02:00
timothy ef615f8c69 Merge main: CI MySQL host-port unpin (collision fix, #236)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m56s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m50s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 01:10:45 +02:00
timothyandClaude Fable 5 5918e947d9 test(schedules): pin FixedStartTimeBehavior + FixDiscardToFillAttempts in round-trip (F7)
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 2s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m1s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Seed a non-null FixedStartTimeBehavior (Flexible) on the Fixed-start item and
assert it survives envelope A; assert the Marathon-order Duration item's
DiscardToFillAttempts is zeroed by the deliberate FixDiscardToFillAttempts
server normalization (Random/Shuffle keep the value, all else -> 0).

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