Commit Graph
38 Commits
Author SHA1 Message Date
timothy 78262ea4cb Merge remote-tracking branch 'origin/main' into feat/91b-blazor-removal
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m47s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m29s
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) Successful in 4m23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m48s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m8s
2026-07-11 21:17:03 +02:00
timothyandClaude Opus 4.8 408b0deb89 feat(91b): remove legacy Blazor Server UI (#91 phase b)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m39s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The ChicoryTV React SPA (web/, served at /app) now has full parity for every
route the Blazor UI served, so the legacy Blazor Server / MudBlazor UI is
deleted. This is the milestone-capping removal of #91 phase (b).

Deleted: ErsatzTV/Pages/**, Shared/**, ViewModels/** (39 edit VMs),
Validators/** (10 edit-VM validators), App.razor, _Imports.razor,
Locals/{Shared,Pages}/** (Blazor loc resx; Locals/Resources.* kept),
wwwroot/css + wwwroot/lib, libman.json, and the orphaned MultiSelectBaseTests.

Startup.cs (surgical, not wholesale): removed AddRazorPages/AuthorizeFolder,
AddServerSideBlazor, AddMudServices, AddSortable, AddCourier, the HtmlSanitizer
registration, the Blazor-attached OIDC UseAuthentication/UseAuthorization
middleware (per the #206 auth-posture sign-off), MapBlazorHub, and
MapFallbackToPage("/_Host"). Renamed the branch blazor->legacy; it still
co-hosts MapControllers, /docs (Scalar), dev MapOpenApi and the redirect
middleware. Replaced the _Host fallback with a catch-all (MapFallback ->
302 /app) that excludes /api|/artwork|/docs|/openapi (genuine 404) per #204.
Kept all OIDC/JWT/API-key service wiring (inert unless configured; real auth
is #197), ConditionalIptvAuthorizeFilter, ApiKeyAuthorizationFilter.

Pruned 9 now-unused packages (all verified zero remaining consumers) from
Directory.Packages.props + ErsatzTV.csproj: MudBlazor, Heron.MudCalendar,
Blazored.FluentValidation, BlazorSortable, MediatR.Courier.DependencyInjection,
Markdig, HtmlSanitizer, Chronic.Core, NaturalSort.Extension. Also removed the
now-dead #25 razor-Sonar NoWarn.

LegacyUiRedirects: added the 14 /media/sources/* -> /app/libraries/* redirects
(SPA screens landed in #202) and lifted the #204-era /media/sources prefix ban.

Tests: Release build clean; full solution suite green. Updated Startup
source-text tests + added regression coverage that Blazor wiring is gone, the
catch-all is wired, and all 14 media-sources routes redirect.

Docs: blazor-route-parity.md (phase b COMPLETE), decisions.md (removal entry),
CLAUDE.md, contributing.md, README.md all updated in this PR.

Rollback: tag blazor-final is cut on pre-merge main as the first merge action.

Part of #91.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:01:04 +02:00
timothy 22440cc1e6 Merge remote-tracking branch 'origin/main' into feat/253-pr3-diff-scalar
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m21s
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
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
# Conflicts:
#	docs/decisions.md
2026-07-11 20:45:22 +02:00
timothyandClaude Opus 4.8 bb7b189929 feat(91b): wire deep-scan + external-collections buttons into LibrariesScreen
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m35s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m22s
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) Successful in 4m2s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m50s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
Last SPA pre-work before deleting Blazor Libraries.razor (#91 phase b):
wire the shipped scanLibrary(id, deep) + scanCollections(family, id, deep)
clients (F9 API, #235) into LibrariesScreen so the SPA reaches parity with
Libraries.razor's four scan actions.

- Deep Scan Library button on each remote (Plex/Jellyfin/Emby) library row,
  threading `deep` through the existing optimistic-pending/poll hook (quick +
  deep share the per-library lock).
- External Collections section (quick + deep per remote source). Rows derive
  client-side from getMediaSources(): the media-sources API handler already
  filters each source's `libraries` to sync-enabled entries, so a remote
  source with a non-empty libraries list is exactly GetExternalCollections's
  Libraries.Any(ShouldSyncItems) filter — no new endpoint.
- useCollectionsScan hook: collections scans have no scan-status poll surface
  (the endpoint is library-keyed; Blazor observed collections locks via
  in-process IEntityLocker events), so pending is optimistic + timeout-bounded
  (409 benign, 404/network surfaces the error). Follow-up #271 for a proper
  collections status surface.

Pure SPA change (no backend/OpenAPI). Docs: blazor-route-parity.md §5 (SPA
affordance DONE), decisions.md (derive-vs-endpoint + optimistic-timeout).

Refs #91

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:25:56 +02:00
timothyandClaude Opus 4.8 c40ffefa99 feat(253): PR3 optimistic-concurrency fan-out — Diff + Scalar aggregates
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Fans the frozen Block recipe (api-conventions §7a) across the five Diff/Scalar
replace-all endpoints, completing the #253 PR2→PR4 arc's implementable core:

- #6 Collection custom-order, #7 Playout alternate-schedules, #8 Playout templates
  (shared Playout.Version), #9 MultiCollection, #10 RerunCollection — each: pre-check
  412 as a standalone Either after validation (H2, subtype survives the Join flatten),
  unconditional Version++ (M1), guarded save, controller If-Match/ETag/400/412, SPA
  editor ETag round-trip + 412 conflict dialog.
- H1: the two Playout handlers' catch(Exception)→422 restructured so the guard's
  PreconditionFailedError returns before the catch (412, not 422).
- M2: RerunCollection/Collection refresh runs unconditionally on save; MultiCollection
  keeps its name-only→no-rebuild optimization by bumping on the first (name) save.
- H3: UpdateDefaultDecoHandler bulk-bumps Playout.Version via .SetProperty.
- Shared ConcurrencyHeaders.MalformedIfMatchProblem() for the 400 guard.
- Deferred (→ #269): same-root non-bulk sibling config writers' ETag rotation.

Tests: per-handler pre-check/bump concurrency tests (Playout ×2 incl. non-vacuous
racing-save backstop, Rerun, Multi incl. name-only-no-rebuild M2, Collection);
controller tests get a DefaultHttpContext for the header read/write. Docs:
api-conventions §7a fan-out status, spa-conventions §4a list-editor note, decisions.

Refs #253

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:23:48 +02:00
timothy 4aeabada3b Merge remote-tracking branch 'origin/main' into feat/235-async-contract
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m2s
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 / 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 18:53:43 +02:00
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
timothy 13cd00c8fe Merge remote-tracking branch 'origin/main' into feat/254-mutation-hardening
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m49s
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 / Build & test (.NET) (push) Successful in 6m31s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m21s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m10s
# Conflicts:
#	docs/api-conventions.md
#	docs/decisions.md
2026-07-11 18:10:30 +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 a1bd303cce fix(app): post-commit side effects on CancellationToken.None + guide-xml/empty-list hardening (#254)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 18m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 18m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit
sweep (adversarial-reviewer #22/#23), the parallel-safe backend-isolated slice.

audit#22 F4 — standardize post-commit enqueues on CancellationToken.None:
20 command handlers under MediaCollections/, ProgramSchedules/, Playouts/,
Channels/ threaded the request cancellationToken into work that runs AFTER
SaveChangesAsync commits (WriteAsync rebuild/refresh enqueues, mediator.Publish,
reindex, cache Refresh, and post-commit lookups that gate an enqueue). A late
client-disconnect then turns an already-durable commit into a thrown request AND
drops the side effect. Generalizes the #251 deco-handler fix. Excludes
BuildPlayoutHandler (worker/background token, not a client-disconnect token),
the config/FFmpeg multi-upsert handlers (partial-commit case, separate
follow-up), and response-projection reloads (correctly keep the request token).

audit#22 F2 — DeleteChannelHandler/DeletePlayoutHandler now delete the channel
guide {number}.xml through IFileSystem.File.Delete (observable under
MockFileSystem) and BEFORE the commit (a post-commit delete orphans the xml on a
crash; the xml is regenerable on demand, so pre-commit delete is the safe order).

audit#23 F4 — ReplacePlayoutAlternateScheduleItemsHandler rejects an empty item
list in the handler (not only the controller pre-guard) so a direct caller can't
trip the Max()-on-empty crash.

Docs: api-conventions.md §7a (post-commit token convention + boundaries),
decisions.md entry (rationale, sweep scope, #253 PR2-4 coordination note).
Tests: guide-cache-delete-through-FS for both delete handlers, empty-list guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:42:45 +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 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 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 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 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 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
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
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 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
timothyandClaude Fable 5 dbc47f4cae 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
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>
2026-07-10 23:38:47 +02:00
timothyandClaude Fable 5 28910ff557 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
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>
2026-07-10 23:34:23 +02:00
timothyandClaude Fable 5 50911eb456 fix(spa): review + E2E fixes — schedule-kind gating, portal body font, system-playlist filter, seasons browse, feedback/UX nits (#208 #209)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m9s
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
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:51:49 +02:00
timothy a2ed834ef6 docs: parity verdicts + shared add-to decision entry + spa-conventions §5c (#208 #209)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m15s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-10 00:19:02 +02:00
timothyandClaude Fable 5 f3d8ac559f docs: parity verdict rows for #210/#211/#213 + decision entries (schedule-reset drop, reorder buttons)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:42:09 +02:00
timothy 3bccb568ad feat(spa): small parity nits — block copy, watermark copy, trash select-all, trakt copy fix (#213)
- BlocksScreen: add copy-to-group dialog (mirrors TemplatesScreen), wiring the
  already-existing copyBlock() API client
- WatermarksScreen: add copy via /add?from={id} prefill (mirrors
  FFmpegProfilesScreen), no new endpoint needed
- TrashScreen: add Select all / Clear selection to the action bar; bump
  PAGE_SIZE to the search API's max (100) — true paging needs a page param
  the API doesn't have yet
- TraktListsScreen: fix stale callout text — "View matched items" now opens
  the SPA's own search screen, not the Classic UI

docs/decisions.md: record the YAML-validator paste-textarea deviation, the
channel-number prompt-renumber deviation, extend the "table not calendar"
convention to the deco-templates editor, and document the trash 100-item cap.
2026-07-09 22:40:08 +02:00
timothyandClaude Fable 5 d9faa8be7b docs: spec-follows-runtime naming decision + casing wart (#198)
- api-conventions.md §5a: runtime Newtonsoft casing vs generated spec, the
  schema transformer that mirrors it, and the contract test guarding it.
- decisions.md: append the "wire format is source of truth; spec follows via the
  real contract resolver" decision.
- spa-conventions.md §4: trust the generated key casing; note the removed
  troubleshooting escape hatch and runtime-cased test mocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:00:48 +02:00
timothyandClaude Fable 5 284b65a936 docs: #145 review nits — media-sample gating decision, entry-point note
- decisions.md: Download Media Sample is gated while a session is active in
  the SPA (Blazor only gated Download Results) — intentional deviation.
- blazor-route-parity.md: playback row notes entry points are the channels
  table + movie detail page only for now (other media kinds need a hand-built
  ?mediaItem= URL).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:52:35 +02:00
timothyandClaude Fable 5 add5d4c11d docs: record #145 SPA playback troubleshooting parity, decisions, HLS convention
- blazor-route-parity.md: move /system/troubleshooting/playback from Section 3
  (BLAZOR-ONLY) to Section 2 (SPA-ready); Section 3 now has no blocking rows.
- decisions.md: poll-based completion feedback (no SPA push channel) and the
  datetime-local vs Chronic deviation.
- spa-conventions.md: new HlsPlayer reusable-component + test-mocking note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:03:46 +02:00
timothyandClaude Fable 5 50ae0a7f3b docs: onboarding/convention docs part 1 + handoff past #180/#182/#183 (parity endgame)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m52s
Adds docs/README.md (index), api-conventions.md, spa-conventions.md, e2e-local.md +
scripts/e2e-local.sh, blazor-route-parity.md (#91 phase-b tracker), domain-model.md,
decisions.md. Rule (CLAUDE.md): read these at session start instead of re-recon; update
in the same PR that changes a convention/route/decision. Part 2 = #185.

Handoff: #180/#141/#158/#161 closed (PRs #181/#182/#183); #145 playback-only; #91
readiness plan posted; next prompt = #185 + quick wins + #155/#151/#152/#153.

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