- libraries.ts: scanLibrary(id, deep), new scanCollections(source, id, deep), corrected stale scanShow status-code comment (400 -> 202/404/409/422) - playouts.ts: resetAllPlayouts returns typed ResetAllPlayoutsResponseModel body - libraries.test.ts: deep-scan + scanCollections client tests - decisions.md: #235 async-op contract + F9 endpoints + accepted-by-design channels note - blazor-route-parity.md §5: F9 API gate closed; SPA deep/collections buttons = removal-PR work Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
28 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 now matches in two tiers (ersatztv#204): the exact-path Map
(52 entries — the 13 foundational routes below plus the #204 (A)/(B)/(E-base) additions) and an
ordered list of 36 PatternRule segment-template rules ((C)/(C2)/(D)/(E-page)) for the
parameterized routes. Together they cover every SPA-ready route (all rows formerly in Section 2).
Foundational 13 (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).
#204 route migrations (moved here from Section 2 when their redirects landed). Parameterized
routes: {id} = strict positive integer (non-int/0/negative/overflow falls through to Blazor);
/media/*/page/{n} paging collapses to the SPA default page (the page param is merged into the
target query and harmlessly ignored). The incoming query string is merged into ?kind= targets via
AppendQueryString (&-join). See docs/decisions.md (2026-07-11, #204) and §5 of the design.
| Blazor route | Blazor file | SPA route | Notes |
|---|---|---|---|
/channels/{Id:int?} |
ChannelEditor.razor |
/app/edit-channel/{id} |
allowSubPaths; external logo URL field (mutual exclusion with upload), enumerated language/credits-template/stream-selector pickers, and bare-channel create (ChannelsScreen's "New blank channel") landed #212 |
/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 |
/system/troubleshooting/playback |
Troubleshooting/PlaybackTroubleshooting.razor |
/app/troubleshooting/playback |
covered by #145 — no nav entry; entry points are the Channels table Troubleshoot action (?channel={id}) the movie detail page, and per-episode Troubleshoot actions on season detail pages (?mediaItem={id}, #209) — remaining kinds (music videos, songs, …) still need a hand-built ?mediaItem={id} URL |
/blocks, /blocks/{Id:int} |
Blocks.razor, BlockEditor.razor |
/app/blocks(/{id}) |
allowSubPaths; #144 S1; list search/filter (#213) added 2026-07-11 |
/templates, /templates/{Id:int} |
Templates.razor, TemplateEditor.razor |
/app/templates(/{id}) |
allowSubPaths; #144 S2; list search/filter (#213) added 2026-07-11 |
/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/add, /schedules/{Id:int}, /schedules/{Id:int}/items |
ScheduleEditor.razor, ScheduleItemsEditor.razor |
/app/schedules |
#207; id-dropping (single SPA screen) |
/media/multi-collections(/add, /{Id}/edit) |
MultiCollections.razor, MultiCollectionEditor.razor |
/app/multi-collections |
SPA DONE (MultiCollectionsScreen; in-screen list↔editor via local state, no sub-paths); #151 |
/media/rerun-collections(/add, /{Id}/edit) |
RerunCollections.razor, RerunCollectionEditor.razor |
/app/rerun-collections |
SPA DONE (RerunCollectionsScreen; in-screen list↔editor via local state, no sub-paths); #152 |
/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/playlists |
SPA DONE (#153): group tree + playlist item editor + playout preview (PlaylistsScreen). Note: the /{Id} playlist-editor sub-path is NOT redirected (SPA is in-screen, no sub-path); only /media/playlists redirects |
/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/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=seasons |
seasons browsable as a top-level kind (MediaBrowseScreen; also reachable via show drill-in); #209 review fix |
/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?kind=episodes |
standalone SPA episode browse EXISTS (MediaBrowseScreen, generic grid, top-level episodes kind); episode cards there and on the Search screen now navigate to the season detail page and anchor/highlight the episode (/app/media/seasons/{seasonId}#episode-{id}), matching Search.razor:241's media/tv/seasons/{SeasonId}#episode-{EpisodeId} link (LibraryBrowseItemResponseModel.SeasonId, mediaDetailPath); #220. Note: only the page browse redirects; individual episode detail has no dedicated SPA route |
/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 2 — SPA-READY, not yet redirected
Mutation-depth verification (2026-07-09, #203 sweep): "SPA-READY" previously meant only "screen exists". A cold adversarial review (#91 gate, findings #202–#206) proved that insufficient, so every row below was re-verified capability-by-capability (Blazor page mutations vs SPA screen + REST endpoint; accepted deviations in
docs/decisions.mdexcluded). Verdicts:
Cluster Verdict Gap issue Troubleshooting (4 routes) PARITY-OK (block-history page-size persistence + Id>=0 History gating added 2026-07-11) — Blocks/Templates/Decos/Deco-templates PARITY-OK (block-copy UI added 2026-07-09; blocks/templates list search/filter added 2026-07-11) — Filler presets / Trakt / FFmpeg profiles PARITY-OK — Watermarks PARITY-OK (copy via /add?from=prefill, 2026-07-09)— Playout creation + alternate-schedules PARITY-OK — Playout kind-editors + list actions PARITY-OK (delete/reset/erase/scheduling-context wired + templates preview calendar, 2026-07-09; EntityLocker build-lock gating enforced server-side via 409 + mirrored in SPA IsLocked, 2026-07-10 #215)— Multi/rerun collections, playlists, trash PARITY-OK (trash select-all/clear + per-kind "see all" paging past 100, 2026-07-11) — Collections PARITY-OK (custom-order endpoint + reorder UI, all-10-kind add picker, 2026-07-09) — Channel editor PARITY-OK (external logo URL mutual-exclusion, bare-create defaults, enumerated pickers — 2026-07-11) #212 Channels-numbers / Logs PARITY-OK (logs sort + page-size persistence added, 2026-07-11) — Search PARITY-OK (card nav incl. episode cards — #220; per-card/multi-select add-to, add-all, save-as-smart-collection; mutation controls + Add-all gated during refetch — #221; 2026-07-11) — Media browse/detail (read paths + image browser) PARITY-OK — Media browse/detail (mutations, per-show scan, episode info/troubleshoot) PARITY-OK (shared Add-to layer + scan/info/troubleshoot wired, 2026-07-10; mutation controls gated on kind/query/page refetch — #221) — Schedules editors PARITY-OK (full item + schedule CRUD rebuild, draft/explicit-Save, all Blazor fields/gates/resets; 2026-07-11) #207 DONE Media sources PARITY-OK (#202 DONE 2026-07-11 — moved back from Section 3; new SPA screens under /app/libraries/*over a new REST write API)— Bold issues are MUST-FIX gates for #91 phase (b); #212 DONE 2026-07-11; #213 closed 2026-07-11; #202 DONE 2026-07-11.
As of ersatztv#204, every route the mutation-depth sweep marked PARITY-OK received a redirect entry
(an exact Map entry or a Tier-2 PatternRule) and moved to Section 1 — EXCEPT the 14
/media/sources/* routes. Those became SPA-READY as of #202 (DONE 2026-07-11): new screens under
/app/libraries/* (LibrariesScreen hub + LocalLibraryEditScreen, PlexSourceScreen,
RemoteSourceScreen, RemoteConnectionEditScreen, RemoteLibrariesEditScreen,
PathReplacementsEditScreen) over a new REST write API. They are listed below; their redirect entries
are deferred to the #91b removal PR (matching the pre-removal spot-check convention). Once those
redirects land, this section is empty.
| Blazor route | Blazor file | SPA route | Notes |
|---|---|---|---|
/media/sources/local |
LocalLibraries.razor |
/app/libraries |
local-library rows on the hub (list, scan, gear → editor); #202 |
/media/sources/local/add |
LocalLibraryEditor.razor (add mode) |
/app/libraries/local/new |
LocalLibraryEditScreen create mode: name/media-kind/paths (path-exists pre-check); #202 |
/media/sources/local/{Id:int}/edit |
LocalLibraryEditor.razor (edit mode) |
/app/libraries/local/{id} |
LocalLibraryEditScreen edit mode: name + path add/delete (count-confirm)/move (incl. "(New Library)"), media-kind immutable; #202 |
/media/sources/plex |
PlexMediaSources.razor |
/app/libraries/plex |
PlexSourceScreen: OAuth pin-flow sign-in/fix-credentials/sign-out, server table, per-server Refresh; #202 |
/media/sources/plex/{Id:int}/libraries |
PlexLibrariesEditor.razor |
/app/libraries/plex/{id}/sync |
RemoteLibrariesEditScreen (family=plex): sortable Name/MediaKind, per-library sync toggle + Save; #202 |
/media/sources/plex/{Id:int}/paths |
PlexPathReplacementsEditor.razor |
/app/libraries/plex/{id}/path-replacements |
PathReplacementsEditScreen (family=plex); #202 |
/media/sources/jellyfin |
JellyfinMediaSources.razor |
/app/libraries/jellyfin |
RemoteSourceScreen (family=jellyfin): connect/edit-connection/disconnect + server table; #202 |
/media/sources/jellyfin/edit |
JellyfinMediaSourceEditor.razor |
/app/libraries/jellyfin/connection |
RemoteConnectionEditScreen (family=jellyfin): address prefilled, api-key set-affordance (never displayed once set); #202 |
/media/sources/jellyfin/{Id:int}/libraries |
JellyfinLibrariesEditor.razor |
/app/libraries/jellyfin/{id}/sync |
RemoteLibrariesEditScreen (family=jellyfin); #202 |
/media/sources/jellyfin/{Id:int}/paths |
JellyfinPathReplacementsEditor.razor |
/app/libraries/jellyfin/{id}/path-replacements |
PathReplacementsEditScreen (family=jellyfin); #202 |
/media/sources/emby |
EmbyMediaSources.razor |
/app/libraries/emby |
RemoteSourceScreen (family=emby); #202 |
/media/sources/emby/edit |
EmbyMediaSourceEditor.razor |
/app/libraries/emby/connection |
RemoteConnectionEditScreen (family=emby); #202 |
/media/sources/emby/{Id:int}/libraries |
EmbyLibrariesEditor.razor |
/app/libraries/emby/{id}/sync |
RemoteLibrariesEditScreen (family=emby); #202 |
/media/sources/emby/{Id:int}/paths |
EmbyPathReplacementsEditor.razor |
/app/libraries/emby/{id}/path-replacements |
PathReplacementsEditScreen (family=emby); #202 |
Section 3 — BLAZOR-ONLY (blocking issues)
Playlist variant management — API gap #153 RESOLVED
#151 (multi-collections), #152 (rerun collections), and #153 (playlists) are DONE — all three now
have SPA editor screens (/app/multi-collections, /app/rerun-collections, /app/playlists; see
Section 2). The rerun editor offers the REST-supported selection types only (Collection,
MultiCollection, SmartCollection, and the media-item types
TelevisionShow/TelevisionSeason/Artist/Movie/Episode/MusicVideo/OtherVideo/Song/Image/RemoteStream);
Playlist is intentionally excluded, matching RerunCollectionRequestMapping.IsSupportedSelectionType.
#153: the playlist CRUD REST API (/api/playlists/* — groups, playlists, item-list replace, and
draft playout preview) plus the /app/playlists SPA screen (PlaylistsScreen) now mirror
Playlists.razor (group tree, add/rename/delete groups, add/delete playlists) and
PlaylistEditor.razor (per-item Collection Type over the 12 playlist item types — Collection,
TelevisionShow, TelevisionSeason, Artist, MultiCollection, SmartCollection, Movie, Episode, MusicVideo,
OtherVideo, Song, Image; type-conditional playback order; count; Play All; Show In EPG; reorder/copy/
remove; playout preview). IsSystem groups and playlists are read-only in the SPA, matching Blazor.
#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 DONE
Nothing remains here. The playback troubleshooting screen (/app/troubleshooting/playback,
PlaybackTroubleshootingScreen.tsx) is built and now lives in Section 2.
Media sources — #202 DONE (2026-07-11)
Nothing remains here. Found by the #91 cold review (2026-07-09): the SPA /app/libraries screen
listed sources/libraries and could trigger scans, but source add/edit, connection parameters,
path replacements, and library enable/disable had no SPA UI and no write REST API
(MediaSourcesController was GET-only; "Add Source" was a hardcoded disabled stub) — a fresh
install could not connect any library without Blazor.
Resolved by #202: new write controllers (LocalLibrariesController under
/api/libraries/local/*; PlexMediaSourcesController/JellyfinMediaSourcesController/
EmbyMediaSourcesController under /api/media-sources/{plex|jellyfin|emby}/*) wrap the existing
MediatR commands (no new commands, no DB migration), plus new SPA screens
(LocalLibraryEditScreen, PlexSourceScreen, RemoteSourceScreen, RemoteConnectionEditScreen,
RemoteLibrariesEditScreen, PathReplacementsEditScreen) under the now-allowSubPaths
/app/libraries/* route. All 14 Blazor routes now have SPA equivalents — see the per-route table
in Section 2 (moved there from this section). Fixed along the way (pre-existing bugs #202 chose to
own, not just parity-copy): the Plex pin-flow lock leak on an abandoned OAuth flow, cross-source
overwrite in the path-replacement repo SQL, non-finally lock release in sign-out/disconnect
handlers, and the secure apiKey contract (never served over GET). Full design rationale in
docs/decisions.md (2026-07-11 entries) and the capability matrix posted on issue #202.
Schedule editors — #207 DONE (2026-07-11)
| Blazor route | Blazor file | SPA route | Status |
|---|---|---|---|
/schedules/{Id:int}, /schedules/add, /schedules/{Id:int}/items |
ScheduleEditor.razor, ScheduleItemsEditor.razor |
/app/schedules (SchedulesScreen) |
PARITY-OK |
Was (correctly, until #207) a near read-only viewer — no schedule create/edit/delete, every per-item
inspector control hard-coded disabled (~35 Blazor fields unchangeable), add-item defaults-only.
Rebuilt in #207 (web/src/screens/SchedulesScreen.tsx + web/src/schedules/): full schedule
CRUD (create via TopBar/in-screen, edit properties, delete) + full item mutation — every Blazor field,
option list, enable-gate and forced-reset, encoded in the pure itemRules.ts (exhaustively unit-tested)
and applied in the inspector. Uses a draft / explicit-Save model over the single destructive
PUT .../items replace (not the old instant-persist), with Discard + a dirty navigation guard — see
decisions.md 2026-07-11 (a)/(b)/(c) and spa-conventions.md §8. The capability matrix (issue #207)
maps each Blazor field → SPA control → request key → test.
Remaining mutation-depth gaps inside Section 2 rows
Tracked as #91 phase (b) gates without moving whole rows — #212 DONE 2026-07-11 (channel editor),
#213 CLOSED 2026-07-11 — full remainder landed across two branches: block-history page-size
persistence (localStorage key ctv-block-history-page-size, same ctv- namespace as
ctv-theme) + History action gated on block.id >= 0 + client-side name/group filter boxes on
the Blocks and Templates lists (fix/213-spa-nits); logs column sorting + page-size persistence
and trash per-kind "see all" paging (fix/213-logs-trash).
CLOSED 2026-07-09: #210 (playout delete/reset/erase/scheduling-context + preview calendar)
and #211 (collection custom order + all-kind add picker); the block/watermark copy, trash
select-all, and Trakt-note items of #213 landed in the same PR.
CLOSED 2026-07-10: #208 (search mutations: card drill-in, per-card + multi-select add-to,
query-wide Add All via GET /api/search/all-items, Save As Smart Collection) and #209
(media browse/detail: shared Add-to layer web/src/media/addTo/ on tiles + all four detail
pages + child grids, per-show Quick/Deep scan gated to Plex/Jellyfin/Emby, per-episode Media
Info + Troubleshoot entries; POST /api/playlists/{id}/items added). Known accepted deviations
(select-mode toggle, per-card target superset) recorded in docs/decisions.md.
CLOSED 2026-07-10: #221 (adversarial-reviewer#18 follow-up to #208/#209) — those PRs added
mutation actions to two screens whose fetch model keeps the previous result set rendered during a
refetch. On Search and Media browse the per-card Add-to menu, Select/select-mode, selection action
bar, Add-all, and Save-as-smart-collection are now gated while a refetch is in flight (query on
Search; kind/query/page on Media browse), with a visible "Refreshing…" cue and dimmed grid; card
navigation stays live. SearchScreen.addAll also binds its completion to the requesting query so a
late GET /api/search/all-items can no longer open a bulk-add dialog scoped to the previous query.
See docs/spa-conventions.md §3a for the pattern.
CLOSED 2026-07-10: #215 (adversarial-reviewer#18 removal gate) — Blazor's EntityLocker
build-lock gating of per-playout Reset/Erase/Delete/Edit is now enforced server-side: every
id-keyed PlayoutController mutation + ChannelController.ResetPlayout returns 409 while
IsPlayoutLocked(id), and the SPA mirrors the lock via an IsLocked flag on the playout list
DTO (disables the buttons + shows a "Building…" cue). The safety invariant no longer depends on
Blazor, so its removal can't silently drop it. See docs/api-conventions.md §3a + decisions.md.
2026-07-11 (#213, remaining scope): logs sort (GET /api/logs sortField/sortDirection,
clickable column headers) and page-size persistence (client-local localStorage, not a server
ConfigElement) landed; trash "see all" now pages past the 100/kind cap via
GET /api/library/browse (no new API surface — see docs/decisions.md). The sibling branch landed the rest
(block-history page-size/gating, blocks/templates list filters) — #213 fully closed.
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. |
Section 5 — Removal execution runbook (#91 phase b, Step 2/3)
The removal PR is gated — it starts only after these clear: #202 (media-source write API + SPA)
DONE 2026-07-11, #235 F9 (deep-scan + external-collections-scan API) API DONE (#235 slice B):
POST /api/libraries/{id}/scan?deep= now threads deep-scan, and POST /api/media-sources/{plex|jellyfin|emby}/{id}/scan-collections?deep=
covers external-collections scan (the two Libraries.razor parity gaps) — the SPA Libraries.razor port can now
proceed, and the mandatory cold adversarial pass (#91 comment 2026-07-09). Remaining SPA affordance for
the removal PR (API + thin web/src/api/libraries.ts clients — scanLibrary(id, deep), scanCollections —
already shipped by #235): LibrariesScreen currently exposes only quick-scan; the removal PR must add the
deep-scan and external-collections-scan buttons (wiring the shipped clients) before deleting
Libraries.razor, or that capability is lost. (#204's id-carrying pattern
redirects landed 2026-07-11; its catch-all fallback that replaces MapFallbackToPage("/_Host") is folded
into Step 2 below, since it can only ship when _Host is deleted.) When those close, the removal-PR routine
executes, in order:
- Cut the rollback tag
blazor-finalon the pre-deletionmaincommit — the first action, before deleting anything. Exact command + restore path:docs/decisions.md2026-07-11 "Pre-removal Blazor rollback tagblazor-final" (#205). Not av*tag → does not trigger a prod release build. - Delete Blazor per #91's Step 2 recon (comment 2026-07-07):
ErsatzTV/Pages/,Shared/,App.razor, Blazor-onlyViewModels/,_Host.cshtml; stripAddServerSideBlazor/MapBlazorHub/circuits + MudBlazor + jQuery/jQueryUI/Sortable and prune theirDirectory.Packages.propsentries. Surgical, not wholesale, on the BlazorMapWhenbranch (Startup.cs:736-795): it co-hostsMapControllers()+/docs(Scalar) + the legacy-redirect middleware, all of which must survive. ReplaceMapFallbackToPage("/_Host")with the #204 design's catch-all 302→/app(any non-API/artwork/docs path that reaches the end of the former Blazor branch) so no legacy path hard-404s once_Hostis gone. - Auth posture is already signed off — no separate auth step beyond deleting the Blazor-attached OIDC
pieces (
AuthorizeFolder("/")viaAddRazorPages,blazor.UseAuthentication/UseAuthorization). Keep the OIDC/JWT/API-key service wiring (independent gates:ConditionalIptvAuthorizeFilteron/iptv/*,ApiKeyAuthorizationFilteron mutating/api/*). Full analysis + must-not-break list:docs/decisions.md2026-07-11 "Blazor removal auth posture" (#206). Real SPA/API auth = #197. - Verify + docs per #91's Step 3: full build + all test projects (incl. Architecture) + web
test/lint/build; a live-E2E redirect sweep (every legacy route 302s;
/app/*,/iptv/*,/artwork/*unaffected); Docker image build. Flip every row in this doc to REDIRECTED; update CLAUDE.md architecture +docs/contributing.mdBlazor/MudBlazor sections + the Obsidian ErsatzTV doc.
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.