Completes the last collection-editing API gap for #91 phase (b): full playlist management as a REST API and a ChicoryTV SPA editor. (Multi/rerun editors landed in #194; playback troubleshooting #145 is the only remaining functional gate.)
Backend — Playlist CRUD (7d585021)
9 new endpoints on PlaylistController (mirrors MultiCollectionController/CollectionController), reusing existing MediatR handlers plus one new command:
Playlists: GET /api/playlists/{id}, GET .../{id}/items, POST /api/playlists (CreatePlaylistInGroup — renamed to dodge a route-Name collision with ScriptedScheduleController), PUT /api/playlists/{id} (rename + replace items), DELETE /api/playlists/{id}, POST /api/playlists/preview
New Application command RenamePlaylistGroup (name validation mirroring CreatePlaylistGroup).
New DTOs: PlaylistItemResponseModel, PlaylistPreviewItemResponseModel, request DTOs (all #nullable enable). PUT re-indexes items by array position (api-conventions §7). Preview TimeSpan → hh:mm:ss InvariantCulture. 404 via controller existence pre-checks; 422 for validation.
SPA — Playlist editor at /app/playlists (79bdd86e)
Group tree (create/rename/delete groups, create/delete playlists) + item editor (per-item CollectionType→picker→playback-order over the 12 playlist item types, Count, Play All, Show-In-EPG, Move Up/Down, Copy, Remove) + playout preview. isSystem groups/playlists render read-only (Preview stays available). Mirrors the RerunCollectionsScreen type-coupling machinery and CollectionsScreen dialogs.
Review hardening (59edec1e, a6064ecc)
Adversarial fork review of both halves. Backend findings fixed:
HIGH — PUT /api/playlists/{id} now guards IsSystem → 422 (a system/generated playlist could previously be renamed/wiped via the API, which the delete path + Blazor already blocked). Root cause noted: LanguageExtensions.Apply collapses the NotFoundError subtype, so the controller pre-check is what yields a real 404 — also fixed a pre-existing missing→422 lie on the group PUT.
MEDIUM — system playlist-group rename guarded → 422 (controller + defense-in-depth handler).
MEDIUM — POST /api/playlists/preview validates the draft's per-type required ids at the controller boundary → 422 instead of a possible 500.
SPA Low — playlist tree uses Promise.allSettled so one group's transient fetch failure shows that group empty rather than blanking the whole tree.
SPA review otherwise clean (no aliasing on Copy, correct per-item id, reorder = saved order, edit-load hardening).
Tests / docs / verification
Backend: 913 ErsatzTV.Tests pass (24 new PlaylistControllerTests + OpenAPI contract rows + new PlaylistGroupHandlerTests). SPA: 369 web tests, lint, build, check:api all green. Full solution builds clean.
Closes #153.
Completes the last collection-editing API gap for #91 phase (b): full playlist management as a REST API **and** a ChicoryTV SPA editor. (Multi/rerun editors landed in #194; playback troubleshooting #145 is the only remaining functional gate.)
## Backend — Playlist CRUD (`7d585021`)
9 new endpoints on `PlaylistController` (mirrors `MultiCollectionController`/`CollectionController`), reusing existing MediatR handlers plus one new command:
- Groups: `POST/PUT/DELETE /api/playlists/groups[/{id}]` (create / rename / delete)
- Playlists: `GET /api/playlists/{id}`, `GET .../{id}/items`, `POST /api/playlists` (`CreatePlaylistInGroup` — renamed to dodge a route-Name collision with `ScriptedScheduleController`), `PUT /api/playlists/{id}` (rename + replace items), `DELETE /api/playlists/{id}`, `POST /api/playlists/preview`
- New Application command `RenamePlaylistGroup` (name validation mirroring `CreatePlaylistGroup`).
- New DTOs: `PlaylistItemResponseModel`, `PlaylistPreviewItemResponseModel`, request DTOs (all `#nullable enable`). PUT re-indexes items by array position (api-conventions §7). Preview TimeSpan → `hh:mm:ss` InvariantCulture. 404 via controller existence pre-checks; 422 for validation.
## SPA — Playlist editor at `/app/playlists` (`79bdd86e`)
Group tree (create/rename/delete groups, create/delete playlists) + item editor (per-item CollectionType→picker→playback-order over the 12 playlist item types, Count, Play All, Show-In-EPG, Move Up/Down, Copy, Remove) + playout preview. `isSystem` groups/playlists render read-only (Preview stays available). Mirrors the `RerunCollectionsScreen` type-coupling machinery and `CollectionsScreen` dialogs.
## Review hardening (`59edec1e`, `a6064ecc`)
Adversarial fork review of both halves. Backend findings fixed:
- **HIGH** — `PUT /api/playlists/{id}` now guards `IsSystem` → 422 (a system/generated playlist could previously be renamed/wiped via the API, which the delete path + Blazor already blocked). Root cause noted: `LanguageExtensions.Apply` collapses the `NotFoundError` subtype, so the controller pre-check is what yields a real 404 — also fixed a pre-existing missing→422 lie on the group PUT.
- **MEDIUM** — system playlist-group rename guarded → 422 (controller + defense-in-depth handler).
- **MEDIUM** — `POST /api/playlists/preview` validates the draft's per-type required ids at the controller boundary → 422 instead of a possible 500.
- SPA **Low** — playlist tree uses `Promise.allSettled` so one group's transient fetch failure shows that group empty rather than blanking the whole tree.
SPA review otherwise clean (no aliasing on Copy, correct per-item id, reorder = saved order, edit-load hardening).
## Tests / docs / verification
- Backend: **913 `ErsatzTV.Tests` pass** (24 new `PlaylistControllerTests` + OpenAPI contract rows + new `PlaylistGroupHandlerTests`). SPA: **369 web tests**, lint, build, `check:api` all green. Full solution builds clean.
- OpenAPI spec + endpoint-index + SPA client regenerated (committed).
- **Live-E2E passed** (`scripts/e2e-local.sh` + Playwright): group/playlist CRUD, item add/reorder/copy/remove, save+reload persistence, type-coupling, 12-type dropdown, graceful 422 on preview — zero unexpected console errors.
- `docs/blazor-route-parity.md` #153 rows → **SPA DONE**; `docs/domain-model.md` playlist row → `/app/playlists`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add write endpoints to PlaylistController for the playlist editor: group
create/rename/delete, playlist create/read/update/delete, item list read,
and draft playout preview. Introduces a RenamePlaylistGroup command/handler
(the one missing Application-layer operation) plus request/response DTOs.
Endpoints (verb / route / route-Name):
- POST /api/playlists/groups CreatePlaylistGroup
- PUT /api/playlists/groups/{id} UpdatePlaylistGroup (rename)
- DELETE /api/playlists/groups/{id} DeletePlaylistGroup
- GET /api/playlists/{id} GetPlaylistById
- GET /api/playlists/{id}/items GetPlaylistItems
- POST /api/playlists CreatePlaylistInGroup
- PUT /api/playlists/{id} UpdatePlaylist (rename + replace items)
- DELETE /api/playlists/{id} DeletePlaylist
- POST /api/playlists/preview PreviewPlaylist
404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a
controller-side existence pre-check (mirrors TemplateController.DeleteGroup),
leaving existing shared handlers untouched; validation failures return 422.
RenamePlaylistGroup returns NotFoundError -> 404 for a missing group.
Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA
client types (web/src/api/generated/v1.d.ts). No SPA screen in this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a ChicoryTV SPA Playlist editor at /app/playlists over the new
playlist-CRUD REST API: a group tree (add/rename/delete groups,
add/delete playlists) plus a playlist item editor (per-item Collection
Type over the 12 playlist item types, type-conditional playback order,
count, Play All, Show In EPG, reorder/copy/remove) and a draft playout
preview. System groups and playlists are read-only, mirroring the Blazor
Playlists.razor / PlaylistEditor.razor pages.
- web/src/api/playlists.ts: group/playlist/item CRUD + preview client fns
- web/src/screens/PlaylistsScreen.tsx: group tree + editor + preview
- App.tsx: route, ScreenId, nav, dispatch wiring
- docs: blazor-route-parity + domain-model updated to SPA DONE
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardening from adversarial review of the #153 playlist API:
- PUT /api/playlists/{id}: guard IsSystem in the controller after the
existence pre-check -> 422, so a system (generated) playlist can no
longer be renamed/wiped. ReplacePlaylistItems is never sent for it.
- PUT /api/playlists/groups/{id}: add controller existence pre-check
(404 for missing, mirroring DeleteGroup) plus an IsSystem 422 guard;
RenamePlaylistGroupHandler also gains a system guard (defense-in-depth
for the Blazor path). Missing/system are now distinct outcomes despite
LanguageExtensions.Apply collapsing NotFoundError to a plain BaseError.
- POST /api/playlists/preview: validate each draft item at the controller
boundary (the id required for its collection type must be present) ->
422 before the shared PreviewPlaylistPlayoutHandler runs, preventing a
NRE/500 in the playout builder. Logic lives in ReplacePlaylistRequest so
it stays parallel with ReplacePlaylistItemsHandler's PUT-path check.
Tests: controller cases for system-playlist PUT, system-group PUT,
missing-group 404, and invalid-preview 422 (each asserting the handler is
not invoked); handler tests for RenamePlaylistGroup system/missing/success.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use Promise.allSettled when loading each group's playlists so one group's
transient error shows that group empty rather than blanking the whole tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Closes #153.
Completes the last collection-editing API gap for #91 phase (b): full playlist management as a REST API and a ChicoryTV SPA editor. (Multi/rerun editors landed in #194; playback troubleshooting #145 is the only remaining functional gate.)
Backend — Playlist CRUD (
7d585021)9 new endpoints on
PlaylistController(mirrorsMultiCollectionController/CollectionController), reusing existing MediatR handlers plus one new command:POST/PUT/DELETE /api/playlists/groups[/{id}](create / rename / delete)GET /api/playlists/{id},GET .../{id}/items,POST /api/playlists(CreatePlaylistInGroup— renamed to dodge a route-Name collision withScriptedScheduleController),PUT /api/playlists/{id}(rename + replace items),DELETE /api/playlists/{id},POST /api/playlists/previewRenamePlaylistGroup(name validation mirroringCreatePlaylistGroup).PlaylistItemResponseModel,PlaylistPreviewItemResponseModel, request DTOs (all#nullable enable). PUT re-indexes items by array position (api-conventions §7). Preview TimeSpan →hh:mm:ssInvariantCulture. 404 via controller existence pre-checks; 422 for validation.SPA — Playlist editor at
/app/playlists(79bdd86e)Group tree (create/rename/delete groups, create/delete playlists) + item editor (per-item CollectionType→picker→playback-order over the 12 playlist item types, Count, Play All, Show-In-EPG, Move Up/Down, Copy, Remove) + playout preview.
isSystemgroups/playlists render read-only (Preview stays available). Mirrors theRerunCollectionsScreentype-coupling machinery andCollectionsScreendialogs.Review hardening (
59edec1e,a6064ecc)Adversarial fork review of both halves. Backend findings fixed:
PUT /api/playlists/{id}now guardsIsSystem→ 422 (a system/generated playlist could previously be renamed/wiped via the API, which the delete path + Blazor already blocked). Root cause noted:LanguageExtensions.Applycollapses theNotFoundErrorsubtype, so the controller pre-check is what yields a real 404 — also fixed a pre-existing missing→422 lie on the group PUT.POST /api/playlists/previewvalidates the draft's per-type required ids at the controller boundary → 422 instead of a possible 500.Promise.allSettledso one group's transient fetch failure shows that group empty rather than blanking the whole tree.SPA review otherwise clean (no aliasing on Copy, correct per-item id, reorder = saved order, edit-load hardening).
Tests / docs / verification
ErsatzTV.Testspass (24 newPlaylistControllerTests+ OpenAPI contract rows + newPlaylistGroupHandlerTests). SPA: 369 web tests, lint, build,check:apiall green. Full solution builds clean.scripts/e2e-local.sh+ Playwright): group/playlist CRUD, item add/reorder/copy/remove, save+reload persistence, type-coupling, 12-type dropdown, graceful 422 on preview — zero unexpected console errors.docs/blazor-route-parity.md#153 rows → SPA DONE;docs/domain-model.mdplaylist row →/app/playlists.🤖 Generated with Claude Code
Add write endpoints to PlaylistController for the playlist editor: group create/rename/delete, playlist create/read/update/delete, item list read, and draft playout preview. Introduces a RenamePlaylistGroup command/handler (the one missing Application-layer operation) plus request/response DTOs. Endpoints (verb / route / route-Name): - POST /api/playlists/groups CreatePlaylistGroup - PUT /api/playlists/groups/{id} UpdatePlaylistGroup (rename) - DELETE /api/playlists/groups/{id} DeletePlaylistGroup - GET /api/playlists/{id} GetPlaylistById - GET /api/playlists/{id}/items GetPlaylistItems - POST /api/playlists CreatePlaylistInGroup - PUT /api/playlists/{id} UpdatePlaylist (rename + replace items) - DELETE /api/playlists/{id} DeletePlaylist - POST /api/playlists/preview PreviewPlaylist 404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a controller-side existence pre-check (mirrors TemplateController.DeleteGroup), leaving existing shared handlers untouched; validation failures return 422. RenamePlaylistGroup returns NotFoundError -> 404 for a missing group. Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA client types (web/src/api/generated/v1.d.ts). No SPA screen in this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>