Integration: review-gates batch — #220/#215/#217/#219/#218 + #213 remainder (PRs #223–#228) #239

Merged
timothy merged 23 commits from integrate/review-gates into main 2026-07-11 02:03:47 +02:00
Owner

Integrates the six remaining review-gate PRs (#223, #224, #225, #226, #227, #228) in one CI round after #222 merged — they conflicted pairwise on docs (blazor-route-parity.md, decisions.md), resolved here with the planned #227/#228 reconciliation (all #213 items declared closed, stale gap-cells swept). OpenAPI regenerated post-merge: zero diff (auto-merge was consistent).

Closes #220, closes #215, closes #217, closes #219, closes #218, closes #213.

Verification on the integrated tree: ErsatzTV.Tests 1024/1024 (TZ=UTC), web 504/504 + tsc + check:api clean.

Individual PRs will auto-mark merged when this lands (their heads become reachable from main).

🤖 Generated with Claude Code

Integrates the six remaining review-gate PRs (#223, #224, #225, #226, #227, #228) in one CI round after #222 merged — they conflicted pairwise on docs (blazor-route-parity.md, decisions.md), resolved here with the planned #227/#228 reconciliation (all #213 items declared closed, stale gap-cells swept). OpenAPI regenerated post-merge: zero diff (auto-merge was consistent). Closes #220, closes #215, closes #217, closes #219, closes #218, closes #213. Verification on the integrated tree: ErsatzTV.Tests 1024/1024 (TZ=UTC), web 504/504 + tsc + check:api clean. Individual PRs will auto-mark merged when this lands (their heads become reachable from main). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 23 commits 2026-07-11 01:53:08 +02:00
fix: episode cards navigate to season detail + anchor (API SeasonId on browse items) (fixes #220)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m23s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
7129d9c5b5
Episode cards in the SPA search and media-browse screens were inert (mediaDetailPath had no
Episode case, and LibraryBrowseItemResponseModel carried no parent-season id to route with).

- API: add nullable SeasonId to LibraryBrowseItemResponseModel; populate it in
  LibraryBrowseItemMapper.GetEpisodes (the single shared hydration site used by both the
  library-browse search/browse handler and the season episode drill-in), leave it null for
  every other kind. Regenerated v1.json + v1.d.ts per docs/api-conventions.md §5.
- SPA: mediaDetailPath now routes Episode items with a seasonId to
  /app/media/seasons/{seasonId}#episode-{id} (matching Blazor's Search.razor:241 link), null
  otherwise. MediaPosterCard accepts an id/highlighted pair; SeasonDetailScreen's episode grid
  gives each card a stable `episode-{id}` anchor and scrolls/highlights it on mount and on
  hashchange (deep-link support).
- Tests: GetLibraryBrowseItemsHandlerTests asserts SeasonId is populated for episode drill-in
  results and null for other kinds; web tests cover mediaDetailPath's episode cases and the
  anchor/scroll/highlight behavior (jsdom scrollIntoView stub).
- Docs: blazor-route-parity.md's episode-browse row and the Search cluster verdict updated —
  the standalone SPA episode browse exists and episode cards now navigate, closing the
  adversarial-reviewer#18 finding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AddItemsToPlaylistHandler only validated existence for movies/shows/
seasons/episodes, leaving artist/music-video/other-video/song/image/
remote-stream ids unchecked (silently accepted, or in RemoteStream's
case silently dropped entirely - the apply dictionary never included
CollectionType.RemoteStream). Mirror AddItemsToCollectionHandler's
established pattern: add RemoteStream to the apply dictionary, and add
an aggregate existence check (ValidateMediaItems/GetRequestedMediaItemIds)
across all ten kinds against dbContext.MediaItems.

Add ErsatzTV.Tests/Application/MediaCollections/PlaylistHandlerTests.cs
covering: a bogus id of each of the ten kinds fails validation; a valid
RemoteStream id is actually persisted to the playlist (regression test
for the drop bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(api): resolve per-show scan by exact show id, not substring title (fixes #219)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
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
8ddf0ae169
POST /api/libraries/{id}/scan-show resolved the target show via
GetShowIdByTitle, an EF.Functions.Like "%title%" substring match with
no OrderBy - non-deterministic under duplicate/overlapping titles and
capable of scanning the wrong show. The Blazor UI never had this bug
(it always passed the exact show id); this endpoint shipped days ago
in PR #216 with no external consumers, so the contract break is safe.

BREAKING CHANGE: ScanShowRequest now takes `showId: int` instead of
`showTitle: string`. Replaced ITelevisionRepository.GetShowIdByTitle
with GetShowTitle(libraryId, showId), which also enforces the show
belongs to the given library. LibrariesController.ScanShow now returns
a genuine 404 ProblemDetails (via ApiResults.NotFoundProblem, the
established pre-check pattern from TemplateController.DeleteGroup)
when the show id doesn't exist in that library, then queues
QueueShowScanByLibraryId with the DB-resolved title.

SPA: libraries.ts ScanShowParams.showId replaces showTitle;
MediaDetailScreen.tsx passes show.id. Extended
ApiErrorResponseMetadataTests and OpenApiErrorResponseContractTests
with the new 404 contract for ScanShow. Regenerated v1.json / v1.d.ts
via scripts/update-openapi.sh + npm run generate:api.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
28910ff557
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>
fix(spa): episode anchor nav must handle in-app popstate + not re-scroll on refetch (#220 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m30s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
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
5b945c308d
Adversarial review of #220 found in-grid episode card clicks never
scrolled/highlighted: navigateToPath() (routing.ts) does pushState +
a synthetic popstate, not a real hash change, so the anchor effect's
hashchange-only listener never fired for same-pathname navigation.
Now listens to both hashchange and popstate.

Also: track the last anchor value actually scrolled to so a
refetch/pagination that recreates the items array (anchor unchanged)
doesn't hijack scroll position; document the known CHILD_PAGE_SIZE
deep-link limitation (parity with the Blazor fragment link); and fix
the MediaPosterCard/shell.css comments that described the highlight
ring as "temporary" when only its glow pulse fades, not the ring
itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
dbc47f4cae
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>
docs(e2e): add local TV library seeding recipe (#220)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 3s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
0156077e18
Document the on-disk media + direct-SQLite LibraryPath + scan recipe for
E2E, since a local library is not API-seedable. Capture two gotchas hit
while verifying the episode-nav PR: deleting search-index/ leaves search
permanently empty (migration doesn't reindex from DB; rescan skips
unchanged files), and /api/search needs field/wildcard queries
(title:Alpha), not bare title words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(spa): remove hard-coded Playouts nav badge (fixes #218)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m49s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
6823b42dbc
The sidebar "Playouts 3" badge on a fresh empty DB was design-mock
scaffolding (badge: 3 hard-coded in the routes array) never wired to
live data; Blazor had no equivalent. Removed the value but kept the
nav-badge mechanism (ScreenRoute.badge, NavItem badge/badgeTone props)
in place since it's a plausible future home for a live warnings count.

The footer "1 failing" chip reported in the same issue is NOT a bug:
summarizeHealth renders live GET /api/health data, and on a fresh
local dev instance the genuinely failing check is FFmpeg Capabilities
(local Homebrew ffmpeg lacks the subtitles/zscale filters that prod's
ffmpeg image has). No code change for that half.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
caeabf2a2b
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>
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
0a8c7b691b
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>
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 10m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m8s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
6b9d76f948
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 5m46s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
9da582924a
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m51s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
83455fa42e
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 6m8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 15m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
b70713fde8
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
086319f200
Merge main: CI MySQL host-port unpin (collision fix)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
d0e431beff
# Conflicts:
#	docs/blazor-route-parity.md
# Conflicts:
#	docs/blazor-route-parity.md
#	docs/decisions.md
# Conflicts:
#	docs/blazor-route-parity.md
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
f0ada7e993
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy merged commit c77ed43585 into main 2026-07-11 02:03:47 +02:00
Sign in to join this conversation.