Commit Graph
2716 Commits
Author SHA1 Message Date
timothyandClaude Opus 4.8 0ff21d5b8e ci: restore bump-prod-compose auto-deploy job on v* release
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m32s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Bump prod compose tag (server-management) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m9s
Build ErsatzTV Image / Bump prod compose tag (server-management) (push) Has been skipped
The auto-pin-to-prod job designed on the unmerged `ci/auto-bump-prod-compose`
branch (3d6ac883) never landed on main — so v* releases (v26.5.0, v26.6.0) did
NOT auto-bump the server-management compose pin (it sat at 26.5.0). The docs
(homelab-docs Docker/ErsatzTV.md, ci-cd.md) described the auto-bump as if live.

Restore the job verbatim (its credentials already exist: the `ersatztv-ci-deploy`
write deploy key, id 5, on server-management + the SERVERMGMT_DEPLOY_KEY secret
here). On a v* tag, after the test-gated image builds, it rewrites the pinned
`ersatztv:<version>` tag in docker/bumblebee/stacks/media-servers/compose.yaml
and pushes to server-management `master` → the Gitea->Komodo webhook redeploys
prod with a pre-deploy backup. Idempotent (no-op if already pinned).

docs/ci-cd.md updated to match (release procedure + the stale ":prod pin" claim,
which was actually an immutable :<version> pin since 2026-07-07).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:44:36 +02:00
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
v26.7.0
2026-07-11 21:17:03 +02:00
timothyandClaude Opus 4.8 3c5d05908a chore(91b): clean up dead Blazor residue (Serilog override + stale comment)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m44s
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
Codex-review nits: drop the now-inert "MudBlazor" Serilog level override in
appsettings.json (package removed) and reword the PlaylistController comment
that referenced the deleted Blazor MultiSelectBase.AddItemsToPlaylist path. No
behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:10:15 +02:00
timothyandClaude Opus 4.8 4407dd53af fix(91b): delete orphaned Blazor NavigationManagerExtensions
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Cold-review Low-1: ErsatzTV/Extensions/NavigationManagerExtensions.cs survived
the removal — the last non-deleted .cs still importing
Microsoft.AspNetCore.Components/JSInterop and calling the deleted
blazorHelpers.scrollToFragment JS. Fully unreferenced (compiled only via the
shared framework). Removing it completes the Blazor deletion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:09:26 +02:00
timothy bb310f6bc7 Merge pull request 'chore(dev): husky pre-commit / pre-push / commit-msg guardrails' (#273) from feat/autolint-precommit into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m47s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 9m27s
blazor-final
2026-07-11 19:07:35 +00: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
timothyandClaude Opus 4.8 7ffc12b213 docs(handoff): lore — activating IsConcurrencyToken exposes all root writers (#253 PR3 review)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m47s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m13s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 20:58:27 +02:00
timothy 2d0935b651 Merge remote-tracking branch 'origin/main' into feat/autolint-precommit
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 20:57:05 +02:00
timothy 6c9b998d7d Merge remote-tracking branch 'origin/main' into feat/autolint-precommit
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m26s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 20:46:01 +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
timothy 550d7ac984 Merge pull request '#253 PR2 — optimistic-concurrency fan-out (Template, DecoTemplate, Playlist, schedule-items)' (#268) from feat/253-pr2 into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m49s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m7s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m38s
2026-07-11 17:54:05 +00:00
timothy f59f2362e2 ci: retrigger — flake in unrelated LibrariesScreen scan-polling test (passes 13/13 locally)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 19:45:07 +02:00
timothy c93266b6fb Merge remote-tracking branch 'origin/main' into feat/253-pr2
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m34s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m28s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 19:44:39 +02:00
timothyandClaude Opus 4.8 e61f58cd99 fix(253): force-write non-participating root writers past a concurrent Version bump (PR3 review)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 2m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review caught a HIGH the fan-out introduced: activating Version as an
IsConcurrencyToken on Playout/Collection makes EF append `WHERE Version=@orig` to
EVERY root UPDATE, so a non-If-Match writer that saves via plain SaveChangesAsync
now throws DbUpdateConcurrencyException → 500 when a replace-all editor bumps the
row between its load and save. Realistic two-tab trigger (edit playout settings while
editing its alt-schedules; edit a collection's name while reordering) — a new crash,
previously silent last-write-wins.

Fix: shared ConcurrencyExtensions.SaveChangesForcingVersion — on a concurrency
failure it adopts the stored token as original+current (client-wins merge scoped to
the token, never reverting the concurrent bump) and retries, i.e. Phase-1 force-write
semantics for a missing If-Match. Applied to the exposed UPDATE writers:
UpdatePlayout, Update{Sequential,Scripted,ExternalJson}Playout, UpdateOnDemandCheckpoint,
UpdateCollection. Non-vacuous test proves the write lands and the bump survives.

Deletes + repo-mediated Add* writers (rarer / join-rows-only) re-scoped onto #269.

Refs #253 #269

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:36:57 +02:00
timothyandClaude Opus 4.8 f27f458ff5 feat(web): add husky pre-commit/pre-push/commit-msg guardrails
Installs husky git hooks (via web/'s lint-staged + npm, since the JS/TS
project lives in web/ with no root package.json) to catch lint, format,
type, and generated-API-drift errors locally before they reach CI.

Hooks (committed at repo root under .husky/):
- pre-commit: (a) lint-staged runs eslint --fix on staged
  web/src/**/*.{ts,tsx} + a project-wide typecheck; (b) if any *.cs are
  staged, dotnet format --verify-no-changes on just those files (skipped
  when no .cs staged, so web-only commits skip the sln load).
- pre-push: CI-parity gate — cd web && check:api && lint && typecheck &&
  build. Blocks pushing drift or a change that breaks an unstaged file.
- commit-msg: requires a Co-Authored-By trailer (merge commits exempt).

Wiring: web/package.json gains husky + lint-staged devDeps, a lint-staged
config, and a `prepare` script (cd .. && husky) that points git's
core.hooksPath at the repo-root .husky dir on npm install. A fresh
`web/` npm install installs all four hooks automatically.

Monorepo/worktree gotchas handled:
- husky init hard-checks for .git in cwd, so `prepare` cd's to the repo
  root before invoking husky (npm keeps web/node_modules/.bin on PATH).
- git exports GIT_DIR while running hooks; in a worktree/subdir that made
  pre-push's `git diff` (check:api) mislocate the working tree and pass
  silently on drift — pre-push now unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE.

docs/ci-cd.md: new "Pre-commit hooks (web/)" section covering all four.

Verified: eslint error blocks commit; clean commit passes; bad-format .cs
blocks (dotnet format ~6-7s scoped), good .cs passes; check:api drift and
a lint error each block `git push --dry-run`, clean state passes; missing
Co-Authored-By blocks commit-msg, present passes; non-web/.cs commits skip
lint/format. npm run lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:31: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 7bc84fa369 fix(#253 PR2): SPA lint — move conflict-reload gating out of loadItems
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m19s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The prior fix set setItemsLoading/setItemsLoaded at the top of loadItems, but the
activeId effect calls loadItems synchronously → react-hooks/set-state-in-effect lint
error (Main Lint SPA, the real CI failure). Move the not-loaded/loading gating into
reloadAfterConflict (an event handler, lint-clean), which is exactly the 412 conflict-
reload path Codex's Medium-3 targeted; canEdit stays false through that reload window.
Behavior unchanged; the normal switch/initial-load paths (guarded separately by the
#242 dirty-guard) are left untouched. Local: lint clean, vitest 667, check:api no drift,
full dotnet solution test green.
2026-07-11 19:15:45 +02:00
timothy 1a24298105 fix(#253 PR2): close review findings (ETag/items consistency + SPA load ordering)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent Codex review of #268 found two Blockers the fork missed + two Mediums:
- Blocker: replace PUTs returned the handler's item snapshot but re-queried the root
  for the ETag separately, so a racing writer could pair stale items with a newer ETag
  (silent overwrite). All four controllers now reload root-then-items (version-first,
  fail-safe) and 404 when the root is gone between commit and reload — matching the
  Block reference. Fixes the Blocker + the Medium '200 without ETag' case together.
- Blocker: PlaylistsScreen loaded items+root via Promise.all (concurrent), pairing a
  stale name with the current ETag; now sequential (items-with-meta first, then root).
- Medium: SchedulesScreen loadItems now marks not-loaded/loading up front so canEdit is
  false through the 412 conflict reload (no stale-draft edits lost).

Controller unit-test mocks updated to stub the new reload query. Full suite green
(ErsatzTV.Tests 1334, web 667, check:api no drift).
2026-07-11 19:08:25 +02:00
timothyandClaude Opus 4.8 3981abc7f9 docs(handoff): lore — enumerate ALL channel/lock producers before a no-cross-release verdict (#235/#267)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m3s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m3s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:07:29 +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 b063bc45c0 docs(#253 PR2): api-conventions §7a — fan-out landed + schedule body-version nuance
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-11 18:43:21 +02:00
timothyandClaude Opus 4.8 589c35d357 fix(235): make QueueShowScanResult switch total (explicit NotFound + throwing fallback)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m59s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Codex-review Low: NotFound previously reached the catch-all by coincidence; a
future enum value would silently 404. Explicit arm + UnreachableException fallback
so an unmapped outcome fails loudly rather than mis-mapping to 404.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:41:33 +02:00
timothy b5c900dfbf chore(#253 PR2): regenerate OpenAPI spec + TS client after fan-out merge 2026-07-11 18:40:43 +02:00
timothy 832d482f2c merge: #253 PR2 schedule-items 2026-07-11 18:38:14 +02:00
timothy 6046bc622e merge: #253 PR2 Playlist 2026-07-11 18:38:05 +02:00
timothy 7b78a393cf merge: #253 PR2 Template+DecoTemplate 2026-07-11 18:37:59 +02:00
timothyandClaude Opus 4.8 787058d18c fix(235): scheduler-safe collections lock ownership (Codex High / Fable reconciliation)
The new POST /api/media-sources/{plex|jellyfin|emby}/{id}/scan-collections
endpoints acquire a per-provider collections lock (409 if held) and hand the
single release to the ScannerService finally. But SchedulerService's periodic
collection scans were enqueued WITHOUT the lock, and ScannerService's finally
released the collections lock whenever held with no ownership check. A
scheduler-queued scan running while an API request held the lock cross-released
the API's lock (#250 bug class), letting a second API request get a spurious
202 instead of 409.

Fix (mirrors the SynchronizePlexLibraryByIdIfNeeded(Unlock: !networksFollow)
library-scan precedent):
- Add `bool Unlock = true` (4th positional param) to the three
  Synchronize{Plex,Jellyfin,Emby}Collections records; default keeps the
  controller + Libraries.razor call sites compiling and releasing on run.
- ScannerService: the three collection finallys now honor `request.Unlock`
  (the concrete typed request is in scope in each method) so a batch member
  with Unlock:false never releases a lock it doesn't own.
- SchedulerService: replace the unlocked per-source enqueue with a lock-once
  per-provider batch — LockX Collections() once, enqueue each source with
  Unlock:isLast (last message owns the release), compensating unlock in catch,
  and SKIP the whole provider loop if the lock is already held. A naive
  "lock-per-source, skip if held" would deterministically starve the 2nd+
  source; lock-once-batch does not.

Tests (ErsatzTV.Tests/Services/): ScannerServiceCollectionLockTests drives the
real ScannerService read loop + real EntityLocker and asserts Unlock:false
leaves a held lock intact while Unlock:true releases (all three providers);
SchedulerServiceCollectionLockTests reflect-invokes ScanPlexMediaSources and
asserts it locks once + skips the enqueue when held, and hands the release to
the last message when acquired. Proven non-vacuous: reverting the Plex fix
fails exactly the three Plex tests.

No OpenAPI/v1.json change (internal channel-message record, not a DTO).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:37:35 +02:00
timothyandClaude Opus 4.8 611924c0ee feat(#253 PR2): optimistic-concurrency contract for Template and DecoTemplate
Wire the frozen ETag/If-Match/412 recipe (Block reference implementation)
onto the Template and DecoTemplate aggregates:

- ReplaceTemplateItems / ReplaceDecoTemplateItems commands gain
  Option<int> ExpectedVersion; ToCommand() on the request DTOs threads it
  through from If-Match.
- Handlers introduce the version check as a standalone Either after
  validation (never via Apply), bump Version unconditionally before
  saving, and persist through SaveChangesWithConcurrencyGuard so a losing
  writer maps to 412 instead of 500. DecoTemplate's post-commit playout
  Reset enqueue now only runs after a successful save.
- TemplateViewModel / DecoTemplateViewModel carry Version (header-only,
  not echoed in the response body), populated in Mapper.
- TemplateController / DecoTemplateController: GET items emits a strong
  ETag of the root's version; PUT parses If-Match (400 on malformed),
  threads the expected version into the command, and returns the new
  ETag from the refreshed root on success. Both PUT actions now use the
  handler's returned item list directly instead of re-querying items.
- SPA: templates.ts / decoTemplates.ts gain getXItemsWithMeta and an
  If-Match-aware replaceX; TemplateEditor / DecoTemplateEditor hold the
  ETag in a ref, read items-with-meta first on load, and open a
  "changed elsewhere" ConfirmDialog on a 412 instead of navigating away.

Tests: new ReplaceTemplateItemsHandlerConcurrencyTests /
ReplaceDecoTemplateItemsHandlerConcurrencyTests mirror the Block
concurrency contract tests (stale/matching/absent If-Match, no-op bump,
racing-save 412, non-vacuous backstop). TemplateControllerTests /
DecoTemplateControllerTests gain ETag/If-Match/412 coverage.
TemplatesScreen.test.tsx / DecoTemplatesScreen.test.tsx gain a 412
conflict-dialog test mirroring BlocksScreen's.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:36:35 +02:00
timothyandClaude Opus 4.8 5c9f04fdec feat(#253 PR2): optimistic-concurrency on schedule-items aggregate
Wire the frozen #253 ETag/If-Match/412 recipe onto ProgramSchedule /
schedule-items, keeping the PR#258 positional in-place reconcile intact.

Backend:
- ReplaceProgramScheduleItems command gains Option<int> ExpectedVersion;
  ReplaceScheduleItemsRequest.ToCommand threads it.
- Handler: standalone CheckVersion Either AFTER validation (so 412 isn't
  flattened to 422), unconditional Version++ before save, guarded save via
  SaveChangesWithConcurrencyGuard, and 412 propagated without running the
  post-save reload/enqueue.
- ProgramScheduleViewModel + Mapper carry Version.
- ScheduleController: GET /items emits ETag; PUT /items parses If-Match
  (malformed -> 400), threads ExpectedVersion, re-queries for the new ETag,
  and advertises 400/412.
- Sibling config-writers (Add/Delete item, Update schedule) bump Version.

Frontend:
- schedules.ts: getScheduleItemsWithMeta + replaceScheduleItems(ifMatch)
  returning ResponseWithMeta.
- SchedulesScreen: etagRef threaded through the #242 dirty-guard (set from
  load + every successful save); 412 opens a conflict ConfirmDialog whose
  Reload discards the draft and re-runs loadItems.

Tests: handler concurrency suite (stale->412 no mutation + fill-group state
untouched, match/absent success+bump, no-op still bumps, racing save->412);
controller ETag/If-Match/412 cases; SchedulesScreen 412-conflict-dialog test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:36:25 +02:00
timothy 1a8c0f60de feat(playlists): wire optimistic-concurrency contract onto Playlist (#253 PR2)
Fans the frozen ETag/If-Match/412 recipe (Block reference, #253) onto the
Playlist aggregate:

- ReplacePlaylistItems command carries ExpectedVersion; the handler runs
  CheckVersion as a standalone Either after validation (so a stale write
  survives as 412, not flattened to 422 by Apply/Join), bumps Version
  unconditionally before saving, and persists via
  SaveChangesWithConcurrencyGuard (EF concurrency-token backstop).
- PlaylistViewModel carries Version; the items GET sets a strong ETag and
  the PUT parses If-Match, threads it into the command, and returns the
  refreshed ETag on success (400 on a malformed If-Match).
- Sibling item-adding handlers (AddItemsToPlaylist, AddMovie/Episode/
  Season/ShowToPlaylist) bump Version too, since they mutate the same
  editor-visible item list.
- SPA: playlists.ts exposes getPlaylistItemsWithMeta and an
  If-Match-aware updatePlaylist; PlaylistEditor holds the ETag in a ref,
  round-trips it on save, and opens a "changed elsewhere" ConfirmDialog on
  412 (mirrors BlockEditor).

Tests: new ReplacePlaylistItemsHandlerConcurrencyTests (stale/match/
force-write/no-op-bump/racing-save), new PlaylistController tests
(ETag on GET items, 400/412/thread-version/force-write on PUT), and a
vitest 412-conflict-dialog test for PlaylistsScreen. dotnet test:
1304/1304 green. web: npm run typecheck clean, npm run build clean,
vitest 664/664 green.

Ref #253 PR2.
2026-07-11 18:33:40 +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 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
timothy 321a34c748 Merge branch 'feat/235-s3-playouts' into feat/235-async-contract 2026-07-11 18:02:58 +02:00
timothy b39f398df7 Merge branch 'feat/235-s4-trakt' 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
timothyandClaude Opus 4.8 d02f953922 fix(235-F7): release leaked Trakt lock on worker shutdown
The global Trakt lock is acquired by SchedulerService.RefreshTraktLists /
MatchTraktLists (and TraktController) and released only when the *terminal*
message of a batch — the one carrying Unlock: true (list == traktLists.Last())
— is processed by WorkerService, whose handler (AddTraktListHandler /
MatchTraktListItemsHandler) calls IEntityLocker.UnlockTrakt() in a finally.

WorkerService.ExecuteAsync breaks out of the read loop on
stoppingToken.IsCancellationRequested (and exits on channel completion /
reader cancellation) BEFORE processing the next message. If shutdown lands
after a batch is enqueued but before its terminal Unlock: true message is
handled, UnlockTrakt() never runs and the in-memory Trakt lock leaks for the
rest of the process lifetime (subsequent Trakt operations 409 forever).

Fix (option a): make the batch-release loss-tolerant with a compensating
release in a finally around the read loop — if the Trakt lock is still held
when the worker stops, release it. Chosen over tracking pending ownership
(b) because the lock is a global singleton and WorkerService is its sole
batch-release site, so "held at shutdown" unambiguously means "the terminal
release was lost"; covers all three exit paths (break / channel completion /
cancellation) in one place. Same lock-lifecycle class as #231/#233/#234.

Regression test: WorkerServiceTests gates the first (non-terminal) batch
message on the stopping token, then StopAsync-cancels so the worker breaks
before the terminal Unlock: true message — asserts the lock is released and
the terminal message was never processed. Proven non-vacuous: inverting the
finally condition fails the test.

Backend-only; no controller/DTO/SPA/OpenAPI impact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:57:41 +02:00
timothyandClaude Opus 4.8 5e5f0af684 fix(235-A): normalize async-op error contracts on Maintenance + Troubleshoot controllers (#235)
Slice A of the async-op API contract normalization.

MaintenanceController:
- EmptyTrash error path: was 500 text/plain (error.ToString()); now maps the
  BaseError Left through ApiResults.ToErrorResult() -> 404 (NotFoundError) / 422
  ProblemDetails. Success stays 200 OkResult. Added ProducesResponseType 200 + 422.
- CleanArtwork: fire-and-forget enqueue of DeleteOrphanedArtwork was a silent 200;
  now returns 202 Accepted (AcceptedResult) since it queues background work.
  Added ProducesResponseType 202. (Controller does not derive from ControllerBase,
  so results are built directly as before.)

TroubleshootController.TroubleshootPlayback (GET|HEAD /api/troubleshoot/playback.m3u8):
- Two bare body-less NotFound() call sites conflated "not found" with "prepare/
  playback failure". Both now return a ProblemDetails body:
  * prepare-failure (result.IsLeft): mapped through error.ToErrorResult() -> 404 for
    NotFoundError (unknown media item/channel) else 422 for a validation BaseError.
  * terminal fall-through (prepare ok but no playable output): kept 404 with a
    distinguishing ApiResults.NotFoundProblem(...) detail.
- Added ProducesResponseType 404 + 422 (409 already present).

Consumer check: the SPA (PlaybackTroubleshootingScreen) feeds the playback.m3u8 URL
straight to hls.js via HlsPlayer, which never inspects the HTTP status code — playback
state is surfaced via the separate /api/troubleshoot/playback/status poll. So the
404->422 split for the validation subcase is safe; no player code branches on the
status code.

Tests: MaintenanceControllerTests (200/422/202 + enqueue assertion),
TroubleshootControllerTests (prepare 404 NotFoundError, 422 validation). All green;
Api error-metadata/contract/security scans still pass.

Note: OpenAPI artifacts (v1.json / v1.d.ts) intentionally NOT regenerated here — the
orchestrator regenerates once after all #235 slices merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:54:21 +02:00
timothyandClaude Opus 4.8 6055bd3687 fix(app): fold Create/DeleteSmartCollection into the post-commit None sweep (#254 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m27s
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
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 35s
Independent adversarial review of PR #266 found two sibling handlers with the
identical post-commit `_smartCollectionCache.Refresh(cancellationToken)` pattern
that the sweep missed (only UpdateSmartCollection was caught). Same audit#22 F4
class: a late client-disconnect after the commit lands would throw and leave the
in-memory smart-collection cache stale vs the committed DB. → CancellationToken.None.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:49:16 +02: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 8e2f995492 Merge pull request 'feat(media-sources): SPA management screens + write REST API (#202)' (#262) from feat/202-media-sources into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m31s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 18m15s
2026-07-11 15:38:24 +00: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