feat(web+api): Trakt lists screen + REST API for SPA parity (#142) #167

Merged
timothy merged 4 commits from feat/142-trakt-lists into main 2026-07-07 15:31:05 +02:00
Owner

Implements #142 (SPA parity: Trakt lists) + API gap #160.

Backend (#160) — new TraktController:

  • GET /api/trakt/lists (paged), GET/{id}, POST (add by URL → 202 Accepted, URL validated synchronously via the AddTraktListHandler regex rules → 422 on bad URL), PUT /{id} (autoRefresh/generatePlaylist, sync), POST /{id}/match → 202, DELETE /{id} → 202.
  • Add/match/delete dispatch to the existing background worker channel gated by the IEntityLocker Trakt lock (same semantics as Blazor); GET /api/trakt/status exposes { busy } so the SPA can poll — the HTTP-observable substitute for the in-process lock. 409 on already-locked mutations.
  • PUT does an explicit existence pre-check (UpdateTraktListHandler silently no-ops on missing id). 21 controller tests + security/contract test entries. No OAuth — public lists via TRAKT__CLIENTID.

Frontend (#142) — TraktListsScreen (/app/trakt-lists, Media nav group): table (slug, name, matched-of-total), add-by-URL dialog, match/refresh + delete actions, sub-path editor /{id} (autoRefresh + generatePlaylist). Polls status at 2.5s only while busy; disables actions and refreshes on busy→idle. "View matched items" deep-links to classic /search?query=trakt_list:{id} until #161's search screen lands.

Verification: build clean; ErsatzTV.Tests 599 green; Core.Tests 493 green (+1 skipped); web typecheck/vitest 202/build/eslint clean. Live E2E: lists/status 200, invalid URL 422, missing-id 404s, real Trakt URL → 202 + background job fails gracefully without TRAKT__CLIENTID (403 logged, status returns to idle, no orphan row), /app/trakt-lists serves. Includes reconcile merge of main (post-#166).

Refs #142, #160.

🤖 Generated with Claude Code

Implements #142 (SPA parity: Trakt lists) + API gap #160. **Backend (#160)** — new `TraktController`: - `GET /api/trakt/lists` (paged), `GET/{id}`, `POST` (add by URL → 202 Accepted, URL validated synchronously via the AddTraktListHandler regex rules → 422 on bad URL), `PUT /{id}` (autoRefresh/generatePlaylist, sync), `POST /{id}/match` → 202, `DELETE /{id}` → 202. - Add/match/delete dispatch to the existing background worker channel gated by the `IEntityLocker` Trakt lock (same semantics as Blazor); `GET /api/trakt/status` exposes `{ busy }` so the SPA can poll — the HTTP-observable substitute for the in-process lock. 409 on already-locked mutations. - PUT does an explicit existence pre-check (UpdateTraktListHandler silently no-ops on missing id). 21 controller tests + security/contract test entries. No OAuth — public lists via `TRAKT__CLIENTID`. **Frontend (#142)** — `TraktListsScreen` (`/app/trakt-lists`, Media nav group): table (slug, name, matched-of-total), add-by-URL dialog, match/refresh + delete actions, sub-path editor `/{id}` (autoRefresh + generatePlaylist). Polls status at 2.5s only while busy; disables actions and refreshes on busy→idle. "View matched items" deep-links to classic `/search?query=trakt_list:{id}` until #161's search screen lands. **Verification**: build clean; ErsatzTV.Tests 599 green; Core.Tests 493 green (+1 skipped); web typecheck/vitest 202/build/eslint clean. Live E2E: lists/status 200, invalid URL 422, missing-id 404s, real Trakt URL → 202 + background job fails gracefully without `TRAKT__CLIENTID` (403 logged, status returns to idle, no orphan row), `/app/trakt-lists` serves. Includes reconcile merge of main (post-#166). Refs #142, #160. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 3 commits 2026-07-07 15:19:56 +02:00
Adds TraktController (GET/POST /api/trakt/lists, GET/PUT/DELETE by id,
POST .../match, GET /api/trakt/status) covering the API gap for SPA
parity issue #142. Add/match/delete dispatch to the same
ChannelWriter<IBackgroundServiceRequest> the classic Blazor "Trakt
Lists" page uses, gated by the existing IEntityLocker (LockTrakt/
IsTraktLocked) singleton; GET /api/trakt/status wraps IsTraktLocked as
the HTTP-observable substitute for the Blazor page's OnTraktChanged
event. URL validation mirrors AddTraktListHandler.ValidateUrl's regexes
(replicated, since that method is private to the handler and returns a
handler-private record) so an obviously-invalid URL gets a synchronous
422 before dispatch.

Adds TraktListResponseModel/PagedTraktListsResponseModel/
TraktStatusResponseModel DTOs, AddTraktListRequest/UpdateTraktListRequest,
controller tests, ApiControllerSecurityTests + OpenApiErrorResponseContractTests
coverage, and regenerates the checked-in OpenAPI document.
Adds web/src/api/trakt.ts wrapping the new /api/trakt/* endpoints
(#160) plus URL-assert tests, and web/src/screens/TraktListsScreen.tsx:
a table (slug, name, match count) with add-by-URL, match/refresh,
delete, and a sub-path editor at /app/trakt-lists/{id} (slug read-only,
autoRefresh/generatePlaylist toggles, save via PUT) - same allowSubPaths
pattern as ChannelEditScreen/SettingsScreen. Since add/match/delete are
async background jobs, the screen polls GET /api/trakt/status while
busy (disabling actions) and refreshes the list on the busy -> idle
transition.

"View matched items" links out to the classic Blazor search page
(/search?query=trakt_list:{traktId}) since the SPA has no search
screen yet (#161) - same interim deep-link pattern used elsewhere.
Registered in App.tsx's Media nav group; SettingsScreen's Classic-UI
help text no longer lists trakt now that it's SPA-native.
Merge remote-tracking branch 'origin/main' into feat/142-trakt-lists
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
1b9d85896c
# Conflicts:
#	ErsatzTV.Tests/Controllers/ApiControllerSecurityTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
timothy added 1 commit 2026-07-07 15:26:03 +02:00
fix: address #167 review — release Trakt lock on failed enqueue; mark busy after generatePlaylist save
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
9c03184707
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy merged commit 063dae731f into main 2026-07-07 15:31:05 +02:00
timothy deleted branch feat/142-trakt-lists 2026-07-07 15:31:05 +02:00
Sign in to join this conversation.