Commit Graph
217 Commits
Author SHA1 Message Date
timothy 3efb2ac4e1 Merge remote-tracking branch 'origin/main' into feat/235-async-contract
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/decisions.md
2026-07-11 18:11:34 +02:00
timothyandClaude Opus 4.8 d32ca976f7 feat(235): SPA clients for deep/collections scan + typed reset-all; docs
- libraries.ts: scanLibrary(id, deep), new scanCollections(source, id, deep),
  corrected stale scanShow status-code comment (400 -> 202/404/409/422)
- playouts.ts: resetAllPlayouts returns typed ResetAllPlayoutsResponseModel body
- libraries.test.ts: deep-scan + scanCollections client tests
- decisions.md: #235 async-op contract + F9 endpoints + accepted-by-design channels note
- blazor-route-parity.md §5: F9 API gate closed; SPA deep/collections buttons = removal-PR work

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:10:02 +02:00
timothy 2de091ea4f Merge pull request '#253 PR1 — optimistic-concurrency contract (infra + Block reference)' (#263)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m38s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
2026-07-11 16:06:11 +00:00
timothyandClaude Opus 4.8 a27cfc475e chore(235): regenerate OpenAPI artifacts + endpoint index after slice merge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:04:33 +02:00
timothy 61c8556ec7 Merge branch 'feat/235-s2-libraries' into feat/235-async-contract 2026-07-11 18:02:58 +02:00
timothyandClaude Opus 4.8 628c9d7228 feat(235): F9 API parity — library deep-scan, external-collections scan, scan-show outcome enum (#235 slice B)
Closes the two F9 Libraries.razor parity gaps and normalizes scan-show error
mapping to ProblemDetails.

TASK 1 — library-wide deep scan:
- QueueLibraryScanByLibraryId gains optional `bool DeepScan = false`; handler
  threads it into ForceSynchronize{Plex,Jellyfin,Emby}LibraryById.
- POST /api/libraries/{id}/scan?deep=false binds it via [FromQuery].

TASK 2 — external-collections scan (new endpoints):
- POST /api/media-sources/{plex|jellyfin|emby}/{id}/scan-collections?deep=false
  acquires the per-source collections lock (§3b: lock IS the running scan → 409),
  enqueues Synchronize{X}Collections(id, ForceScan:true, deep) to the scanner
  channel, returns 202; compensating-unlock on enqueue throw.

TASK 3 — scan-show normalization:
- New QueueShowScanResult enum; handler returns it instead of bool.
- POST /api/libraries/{id}/scan-show now maps 202/404/409/422 (all errors
  ProblemDetails) instead of 200/404/400-anonymous-object.
- Updated the lone Blazor caller (TelevisionSeasonList.razor).

Tests: LibrariesController (scan deep=true, scan-show enum→status), the three
media-source controllers (scan-collections route/404/409/202/compensating-unlock),
and handler tests for both changed handlers (deep threading + show-scan outcomes).
Docs: api-conventions §3b exemplar + blazor-route-parity §5 F9 gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:02:17 +02:00
timothyandClaude Opus 4.8 9b73b62527 feat(235): async-op API contract normalization — playouts slice C (#235)
Slice C of the async-op contract normalization:

- channel reset (POST /api/channels/{channelNumber}/playout/reset) now
  returns 202 Accepted (was 200 Ok) — it only queues a background rebuild
- reset-all (POST /api/playouts/reset-all) still 202 but now returns a
  ResetAllPlayoutsResponseModel body reporting QueuedPlayoutIds /
  SkippedLocked / SkippedUnsupported instead of silently swallowing skips;
  handler returns a new ResetAllPlayoutsResult record
- single-playout GET (GET /api/playouts/{id}) now exposes IsLocked on
  PlayoutResponseModel, set from IEntityLocker.IsPlayoutLocked mirroring
  the list projection — gives a polling client the lock flag

Tests: channel reset asserts 202; reset-all asserts 202 + skipped-body
shape; single GET asserts IsLocked; new ResetAllPlayoutsHandlerTests
(in-memory SQLite) asserts locked/ExternalJson/None land in skipped lists
and eligible playouts in queued. docs/api-conventions.md §3a updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:58:15 +02:00
timothy 7d106bf810 Merge remote-tracking branch 'origin/main' into feat/253-optimistic-concurrency
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/decisions.md
2026-07-11 17:37:12 +02:00
timothy 6ed36b4bac Merge remote-tracking branch 'origin/main' into feat/202-media-sources
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m38s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m59s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/blazor-route-parity.md
#	docs/decisions.md
2026-07-11 17:31:04 +02:00
timothyandClaude Opus 4.8 b285e33747 docs(handoff): lore — prose cross-refs go stale after a parallel merge, re-check them (#205/#206)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m19s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m9s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:22:51 +02:00
timothy 8878bf9e11 docs(review): record deferred If-Match 412-semantics refinement (#265) as an acceptable-defer
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Codex re-review of the fix commit confirmed both prior findings resolved and raised one
new Medium: RFC 7232 would 412 (not 400) a syntactically-valid but non-matching If-Match
(non-canonical "03", weak W/"3", tag lists, empty, overflow). Deferred to #197 (cold
contract pass) as #265 — fail-safe today (the mutation is rejected, never applied) and no
first-party client is affected. Records the deferral where the #253 fan-out will copy the
parser: a code comment in ConcurrencyHeaders + a note in api-conventions §7a.

Refs #253 #265
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:18:35 +02:00
timothyandClaude Opus 4.8 7608cccebd docs(91b): auth-posture sign-off + rollback-tag procedure for Blazor removal (#205, #206)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m35s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
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 / Build & test (.NET) (push) Has been cancelled
Resolve the two SHOULD-FIX gate findings from the #91 cold review by making the
removal plan address them explicitly instead of clearing the gate by omission.

Pages (verified in code, not assumed) — OIDC's AuthorizeFolder("/") gates only the
Blazor _Host Razor Page; /app (SPA) and /api/* were already unauthenticated since
phase (a); /iptv JWT + API-key filters are independent of Blazor and survive
removal. Sign-off: no capability lost, no NEW exposure beyond phase (a); real
SPA/API auth deferred to #197. Recorded in docs/decisions.md.

(cut at removal time on the pre-deletion main commit — not a v* tag, no release
build) + the restore path (checkout+build+pin test container, or revert the merge).
Recorded in docs/decisions.md.

Both fold into a new "Section 5 — Removal execution runbook" in blazor-route-parity.md
so the (gated) removal PR has an ordered checklist. Docs-only; no code change.

refs #205 #206 #91

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:09:29 +02:00
timothyandClaude Opus 4.8 94ebf34ccd feat(api): optimistic-concurrency contract for replace-all PUTs — PR1 infra + Block reference (#253)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds the shared optimistic-concurrency contract so a stale second tab can no longer
silently overwrite a fresher edit. PR1 lands the infra + the Block reference aggregate;
PRs 2–4 fan the same recipe across the other 8 roots (design: #253#issuecomment-8472).

Contract
- `IVersionedAggregate` (`int Version`) on all 9 replace-all roots (ProgramSchedule,
  Block, Template, DecoTemplate, Playlist, Collection, Playout, MultiCollection,
  RerunCollection), EF-mapped `.IsConcurrencyToken()`; one dual-provider migration
  `AddAggregateVersions` (nullable:false, default 0).
- Strong `ETag` of `Version` on the aggregate GET; `If-Match` on the PUT; mismatch →
  412 (distinct from the §3a 409 build-lock guard). Successful PUT returns the new ETag.
- `PreconditionFailedError : BaseError` → 412 in `ApiResults.ToErrorResult`;
  `ConcurrencyHeaders.ParseIfMatch/SetETag`; malformed If-Match → 400; `*`/absent =
  Phase-1 force-write.

Block reference wiring
- Handler: standalone `Either` via `CheckVersion` AFTER validation (never through
  `Apply`, which Join()-flattens the subtype to 422), unconditional `Version++`,
  `SaveChangesWithConcurrencyGuard` backstop (DbUpdateConcurrencyException → 412).
- `BlockViewModel.Version` (header-only, not echoed in the body); controller sets the
  ETag on GET items and on the successful PUT.
- SPA: `client.requestWithMeta` seam; `blocks.getBlockItemsWithMeta` + `replaceBlock`
  If-Match/ETag round-trip; `BlockEditor` holds the ETag, sends If-Match, and on 412
  opens a blocking "changed elsewhere — reload" dialog.

Tests
- Handler contract tests: stale-If-Match → 412 (no mutation), matching/absent → success
  + bump, no-op save still bumps, and a two-context racing save → 412; proven
  non-vacuous (drop `.IsConcurrencyToken()` → the race test fails).
- Controller tests: malformed If-Match → 400, If-Match threaded to the command, ETag on
  GET/PUT, 412 passthrough. SPA: requestWithMeta ETag, replaceBlock If-Match, 412 dialog.

Docs: api-conventions §7a, spa-conventions §4a, domain-model glossary, decisions log.

Refs #253
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:51:59 +02:00
timothy b9a2fdec50 Merge remote-tracking branch 'origin/main' into feat/202-media-sources
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m27s
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
2026-07-11 16:45:03 +02:00
timothy 12cbff01f9 docs(202): parity verdict, domain-model routes, decisions, capability matrix (#202) 2026-07-11 16:43:39 +02:00
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 6ce448d265 feat(spa): media-source SPA foundation — shared client, helpers, App-owned popstate wrapper (#202 slice S5)
Owns the shared single-files so the S6a (Local) / S6b (Remote) editor slices touch
disjoint files. Editor screens are stubbed (MediaSourceEditorPlaceholder) for S6.

- web/src/api/mediaSources.ts (+test): client module over the new media-source write
  endpoints (local CRUD + move/path-exists; Plex pin-flow/sign-out; shared remote
  state/connection/libraries/path-replacements/refresh; family = only URL variance),
  DTOs re-exported from generated v1, messageFromMediaSourcesError; barrel export.
- web/src/mediaSources/{familyMeta,paths,pinFlowPoll}.ts (+tests): family labels/routes/
  remote-path column naming (owns RemoteFamily); client-side NormalizePath mirror for
  in-draft dup detection; pure §C1 pin-flow poll state machine (waiting/finalizing/
  success/timeout/budget-exhausted), timer-free and fully unit-tested.
- routing.ts parseLibrariesSubRoute + LibrariesSubRoute union split Local vs Remote.
- App.tsx: libraries route allowSubPaths; LibrariesRouteScreen wrapper dispatching a
  flat switch to placeholders; App-owned popstate (finding 4) — App is the single
  popstate owner, consults canLeaveCurrentScreen() and only on approval updates
  librariesSubPath passed DOWN to the wrapper (wrapper never self-listens); state write
  scoped to the libraries route so Playouts/Media pops stay byte-identical (nit 3).
- LibrariesScreen hub wiring: Add-Source menu (Local/Plex/Jellyfin/Emby), remote source
  gear -> family screen, local library row gear -> edit route; removed the disabled
  Scan-All button + the deferred-sources card (§C7/§D.1).
- Tests: App-owned-popstate dirty-guard case (confirm false keeps URL+sub-screen; true
  navigates); mediaSources client URL/verb mapping; pinFlowPoll transitions; familyMeta/
  paths units; hub-wiring navigation.
- docs/spa-conventions.md §8 (resolved sub-path+dirty-guard caveat -> App-owned popstate)
  + §2 exemplar list (guarded-route exception).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:11:16 +02:00
timothy c6ca87c06e chore(api): regenerate OpenAPI + TS client for media-source write endpoints (#202 OpenAPI gate) 2026-07-11 15:52:31 +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
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
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
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 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 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
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
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 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
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 0120d3ce5b merge: feat/212-spa-channel into feat/207-212-schedules-channel-depth
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/decisions.md
2026-07-11 00:40:50 +02:00
timothyandClaude Fable 5 5054cd8228 style+docs(spa): schedules editor CSS + parity/decisions/conventions docs (#207)
- shell.css: picker/multiselect/fieldset/toolbar styles for the new editor
- spa-conventions.md §8: navigation-guard convention
- decisions.md: draft/explicit-Save, Copy-includes-refs, shuffled-GET lossiness
- blazor-route-parity.md: schedules cluster -> PARITY-OK (#207 DONE)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:35:05 +02:00
timothy 846452e28c docs: close channel-editor gaps in blazor-route-parity + record #212 decisions
Channel editor cluster moves from GAPS to PARITY-OK in the verdict table now
that external logo URL, bare-create, and enumerated pickers have landed.
Records the bare-create-on-list-screen and external-logo-wins decisions in
decisions.md.
2026-07-11 00:16:15 +02:00
timothyandClaude Fable 5 0a8c7b691b feat(spa): logs sort + page-size persistence, trash see-all paging (#213)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m23s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Blazor parity for the remaining #213 conveniences:

- GET /api/logs gains sortField (timestamp|level) and sortDirection
  (asc|desc) query params, allow-listed and normalized (unrecognized
  values fall back to the pre-existing timestamp-desc default) rather
  than rejected with a 422. LogsScreen.tsx renders clickable, sortable
  column headers with a chevron direction indicator.
- LogsScreen.tsx now persists the chosen page size to localStorage
  (ctv-logs-page-size) and restores it on mount, following the
  existing designSystem.ts localStorage-preference pattern. This is a
  client-local UI preference, not the Blazor ConfigElement-backed
  server setting — see docs/decisions.md.
- TrashScreen.tsx adds a per-kind "See all N ..." affordance that
  pages past the 100/kind /api/search cap using the already-paginated
  GET /api/library/browse (mediaType + pageNum), appending results
  client-side. No new API surface was needed since that endpoint
  already supports the paging the trash screen needed.

docs/decisions.md, docs/blazor-route-parity.md, docs/spa-conventions.md
and docs/api-conventions.md updated in this same commit. OpenAPI spec
regenerated (v1.d.ts unchanged: query params aren't part of the
generated components/schemas surface).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:10:20 +02:00
timothyandClaude Fable 5 caeabf2a2b fix(spa): block-history page-size persistence + History gating, blocks/templates list filter (#213)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Blazor parity conveniences: BlockPlayoutTroubleshootingScreen now persists the block-history
page-size selector to localStorage (ctv-block-history-page-size, same ctv- namespace as
ctv-theme) and restores it on mount, and gates the per-block History action on block.id >= 0
(mirrors BlockPlayoutTroubleshooting.razor, which hides it for synthesized/virtual blocks).
BlocksScreen and TemplatesScreen list screens gain a client-side name/group search filter box,
matching the filter already present on the troubleshooting blocks list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:04:15 +02:00
timothyandClaude Fable 5 223797a6cb docs+openapi: regenerate v1.json/endpoint-index/types + record flat schedule DTO decision (#126 #207 #212)
- Regenerated ErsatzTV/wwwroot/openapi/v1.json, docs/endpoint-index.md, and
  web/src/api/generated/v1.d.ts for the flat schedule-item DTO + discovery endpoints
- docs/decisions.md: flat ScheduleItemResponseModel + NamedIdResponseModel rationale
- docs/api-conventions.md: shared {id,name} embed, polymorphic-VM flattening, and
  optional enum query-filter conventions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:54:12 +02:00