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).
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)
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.IEntityLockerTrakt lock (same semantics as Blazor);GET /api/trakt/statusexposes{ busy }so the SPA can poll — the HTTP-observable substitute for the in-process lock. 409 on already-locked mutations.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-listsserves. Includes reconcile merge of main (post-#166).Refs #142, #160.
🤖 Generated with Claude Code