Adds a paged collection-items endpoint reusing LibraryBrowseItemResponseModel
so the SPA lists a manual collection's full contents (all media kinds), replacing
the lossy Lucene name-based preview. Confirms POST /items already returns 422 for
bogus ids (guarded by ValidateMediaItems, fb3f2856); adds endpoint-level coverage.
fixes #155
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Blazor → SPA route parity tracker
Purpose: the living route-by-route tracker for ersatztv#91 phase (b) — retiring the legacy Blazor Server UI once every route it serves has a ChicoryTV SPA equivalent and a redirect. Update this doc in the same PR that migrates, redirects, or removes any route below — this table is the single source of truth for "what's left."
Sources of truth checked when compiling this table: ErsatzTV/LegacyUiRedirects.cs (redirect map),
web/src/App.tsx (SPA route table), ErsatzTV/Pages/**/*.razor (Blazor pages still present).
How to read this
- REDIRECTED: in
LegacyUiRedirects.cs'sMap— the Blazor route 302s to the SPA route. - SPA-READY (not yet redirected): the SPA screen exists and covers the functionality, but the Blazor route is still reachable directly (no redirect entry yet) — adding the redirect is a small, low-risk PR once the SPA screen has been spot-checked against the Blazor page it replaces.
- BLAZOR-ONLY: no SPA equivalent yet; removing/redirecting this route is blocked on the listed issue(s).
Section 1 — REDIRECTED
(ErsatzTV/LegacyUiRedirects.cs's Map, verified current as of this doc — 13 entries, unchanged
since ersatztv#91 phase (a)/PR #148):
| Blazor route | SPA route |
|---|---|
/ |
/app |
/channels |
/app/channels |
/channels/add |
/app/new-channel |
/schedules |
/app/schedules |
/playouts |
/app/playouts |
/media/libraries |
/app/libraries |
/settings/ffmpeg |
/app/settings/streaming |
/settings/hdhr |
/app/settings/system |
/settings/logging |
/app/settings/logging |
/settings/playout |
/app/settings/playout |
/settings/scanner |
/app/settings/scanner |
/settings/ui |
/app/settings/general |
/settings/xmltv |
/app/settings/xmltv |
Note: the settings sub-routes above are already redirected, not Blazor-only as an earlier draft
of this table implied — LegacyUiRedirects.cs covers all seven /settings/* pages that have SPA
equivalents. There is no Blazor-only settings sub-route left except the ones with no SPA screen at
all (there are none currently — every Blazor Settings/*.razor page has both an SPA screen and a
redirect).
Section 2 — SPA-READY, not yet redirected
Confirmed as of this doc: the SPA screen exists (verified against web/src/App.tsx's route table
and web/src/screens/) but LegacyUiRedirects.cs has no entry for the Blazor route yet.
Scheduling-parity work (#144/#162, DONE 2026-07-07, PRs #170–#175/#179) built the SPA screens for
blocks/templates/decos/deco-templates/playout editors, #145 (PR #182, merged to main) built the
troubleshooting/YAML-validator screens, and #141 (PR #183, merged to main) built the media detail
pages + image folder browser (MediaDetailScreen.tsx's MovieDetailScreen/ShowDetailScreen/
SeasonDetailScreen/ArtistDetailScreen, ImageBrowserScreen.tsx, both dispatched via App.tsx's
MediaRouteScreen sub-route wrapper, same pattern as PlayoutsRouteScreen) — none of these have
been added to the redirect map yet.
| Blazor route | Blazor file | SPA route | Notes |
|---|---|---|---|
/channels/{Id:int?} |
ChannelEditor.razor |
/app/edit-channel/{id} |
allowSubPaths |
/channels/numbers |
ChannelNumbers.razor |
/app/channels |
merged into channels table |
/search |
Search.razor |
/app/search |
|
/system/logs |
Logs.razor |
/app/logs |
|
/system/troubleshooting |
Troubleshooting/Troubleshooting.razor |
/app/troubleshooting |
|
/system/troubleshooting/block-playout |
Troubleshooting/BlockPlayoutTroubleshooting.razor (+BlockPlayoutHistory.razor) |
/app/troubleshooting/blocks |
covered by PR #182 / #145 |
/system/troubleshooting/sequential-schedule |
Troubleshooting/YamlValidator.razor |
/app/troubleshooting/yaml |
covered by PR #182 / #145 |
/blocks, /blocks/{Id:int} |
Blocks.razor, BlockEditor.razor |
/app/blocks(/{id}) |
allowSubPaths; #144 S1 |
/templates, /templates/{Id:int} |
Templates.razor, TemplateEditor.razor |
/app/templates(/{id}) |
allowSubPaths; #144 S2 |
/decos, /decos/{Id:int} |
Decos.razor, DecoEditor.razor |
/app/decos(/{id}) |
allowSubPaths; #144 S3 |
/deco-templates, /deco-templates/{Id:int} |
DecoTemplates.razor, DecoTemplateEditor.razor |
/app/deco-templates(/{id}) |
allowSubPaths; #144 S4 |
/playouts/add(/{kind}) |
PlayoutEditor.razor variants |
/app/playouts |
merged into playouts screen creation flow; #144 S5 |
/playouts/classic/{Id}, /playouts/block/{Id}, /playouts/scripted/{Id}, /playouts/sequential/{Id} |
ClassicPlayoutEditor.razor, BlockPlayoutEditor.razor, ScriptedPlayoutEditor.razor, SequentialPlayoutEditor.razor |
/app/playouts |
merged; #144 S5/S6 |
/playouts/{Id:int}/alternate-schedules |
PlayoutAlternateSchedulesEditor.razor |
/app/playouts/{id}/alternate-schedules |
allowSubPaths (PlayoutsRouteScreen); #144 S6/#162 |
/playouts/{Id:int}/templates |
PlayoutTemplatesEditor.razor |
/app/playouts/{id}/templates |
allowSubPaths (PlayoutsRouteScreen); #144 S6/#162 |
/schedules/{Id:int}, /schedules/add, /schedules/{Id:int}/items |
ScheduleEditor.razor, ScheduleItemsEditor.razor |
/app/schedules |
merged into schedules screen |
/media/filler/presets(/add, /{Id}/edit) |
FillerPresets.razor, FillerPresetEditor.razor |
/app/filler-presets |
allowSubPaths |
/media/collections(/add, /{Id}/edit, /{Id}) |
ManualCollections.razor, CollectionEditor.razor, CollectionItems.razor + SmartCollections.razor/SmartCollectionEditor.razor |
/app/collections |
allowSubPaths |
/media/trash |
Trash.razor |
/app/trash |
|
/media/playlists(/{Id}) |
Playlists.razor, PlaylistEditor.razor |
/app/collections |
merged into collections screen |
/media/trakt/lists(/{Id}) |
TraktLists.razor, TraktListEditor.razor |
/app/trakt-lists(/{id}) |
allowSubPaths |
/ffmpeg(/add, /{Id}) |
FFmpeg.razor, FFmpegEditor.razor |
/app/ffmpeg-profiles |
allowSubPaths |
/watermarks(/add, /{Id}) |
Watermarks.razor, WatermarkEditor.razor |
/app/watermarks |
allowSubPaths |
/media/sources/{local,plex,jellyfin,emby}/... |
LocalLibraries.razor, PlexMediaSources.razor, JellyfinMediaSources.razor, EmbyMediaSources.razor + editors |
/app/libraries |
merged into libraries screen |
/media/movies(/page/{n}) |
MovieList.razor |
/app/media?kind=movies |
generic browse (MediaBrowseScreen); PR #183 / #141 |
/media/movies/{MovieId:int} |
Movie.razor |
/app/media/movies/{id} |
detail page (MovieDetailScreen); PR #183 / #141 |
/media/tv/shows(/page/{n}) |
TelevisionShowList.razor |
/app/media?kind=shows |
generic browse; PR #183 / #141 |
/media/tv/shows/{ShowId:int} |
TelevisionSeasonList.razor |
/app/media/shows/{id} |
show + season list (ShowDetailScreen); PR #183 / #141 |
/media/tv/seasons(/page/{n}) |
TelevisionSeasonSearchResults.razor |
/app/media?kind=shows |
no dedicated season-list SPA screen; covered via show drill-in; PR #183 / #141 |
/media/tv/seasons/{SeasonId:int} |
TelevisionEpisodeList.razor |
/app/media/seasons/{id} |
season + episode list (SeasonDetailScreen); PR #183 / #141 |
/media/tv/episodes(/page/{n}) |
EpisodeList.razor |
/app/media/seasons/{id} |
no standalone SPA episode browse; covered via season detail drill-in; PR #183 / #141 |
/media/music/artists(/page/{n}) |
ArtistList.razor |
/app/media?kind=artists |
generic browse; PR #183 / #141 |
/media/music/artists/{ArtistId:int} |
Artist.razor |
/app/media/artists/{id} |
detail page (ArtistDetailScreen); PR #183 / #141 |
/media/music/videos(/page/{n}) |
MusicVideoList.razor |
/app/media?kind=music-videos |
generic browse; PR #183 / #141 |
/media/music/songs(/page/{n}) |
SongList.razor |
/app/media?kind=songs |
no dedicated SPA song browse beyond generic grid; PR #183 / #141 |
/media/other/videos(/page/{n}) |
OtherVideoList.razor |
/app/media?kind=other-videos |
generic browse; PR #183 / #141 |
/media/remote/streams(/page/{n}) |
RemoteStreamList.razor |
/app/media?kind=remote-streams |
generic browse; PR #183 / #141 |
/media/images(/page/{n}) |
ImageList.razor |
/app/media?kind=images |
generic browse; PR #183 / #141 |
/media/browser/images |
ImageBrowser.razor |
/app/media/images/browser |
interactive image grid picker used by channel editors etc. (ImageBrowserScreen); PR #183 / #141 |
Section 3 — BLAZOR-ONLY (blocking issues)
Multi/rerun collections & playlist variants — API gaps #151/#152/#153
| Blazor route | File | Blocking issue |
|---|---|---|
/media/multi-collections(/add, /{Id}/edit) |
MultiCollections.razor, MultiCollectionEditor.razor |
#151 (multi-collection management API) |
/media/rerun-collections(/add, /{Id}/edit) |
RerunCollections.razor, RerunCollectionEditor.razor |
#152 (rerun-collection management API) |
/media/playlists(/{Id}) editing depth beyond what /app/collections covers |
Playlists.razor, PlaylistEditor.razor |
#153 (playlist variant management API) |
#155 RESOLVED (collection-items enumeration): GET /api/collections/{id}/items (paged) now returns a
manual collection's full contents across all media kinds (reusing LibraryBrowseItemResponseModel), so the
SPA /app/collections items view lists real members instead of the old lossy Lucene collection:"name"
search preview. The POST /api/collections/{id}/items bogus-id case already returns 422 (guarded by
AddItemsToCollectionHandler.ValidateMediaItems), not 500.
Playback troubleshooting — #145
| Blazor route | File | Blocking issue |
|---|---|---|
/system/troubleshooting/playback |
Troubleshooting/PlaybackTroubleshooting.razor |
#145 (playback troubleshooting diagnostics API) — distinct from block-playout troubleshooting, which is already covered (Section 2) |
Section 4 — Blazor home / escape hatch
Not gated on an issue — kept separate from Section 3 because it isn't blocked on anything, just the intentional exit ramp until phase (b) removes Blazor entirely.
| Blazor route | File | Notes |
|---|---|---|
/system/health |
Index.razor |
Blazor's own home page; reachable via Settings → System "Classic UI" link. |
Cross-reference: docs/handoffs/chicorytv-issue-queue.md
Keep this table and that handoff doc in sync at a high level — this table is the detailed route inventory; the handoff doc tracks issue sequencing/session planning. If they disagree on an issue's status, the more recently updated one wins; fix the stale one in the same PR you notice it.