ChicoryTV SPA: Blazor → SPA cutover (retire old UI) #91

Closed
opened 2026-07-02 01:07:19 +02:00 by timothy · 11 comments
Owner

Complete the transition from the Blazor/MudBlazor UI to the SPA — the "fully wired replacement" milestone.

Do

  • Make the SPA the default UI (root route); redirect legacy Blazor page routes.
  • Remove Blazor pages/components as their SPA equivalents ship; drop MudBlazor deps once nothing depends on them.
  • Update docs (CLAUDE.md, docs/) and Docker/host config as needed (infra changes → server-management).

Acceptance: old Blazor admin UI fully replaced by the ChicoryTV SPA; no user-facing MudBlazor screens remain.

Blocked by all screens (Dashboard, Channels, EPG, Schedule, Playouts, Libraries, Channel Builder).

Complete the transition from the Blazor/MudBlazor UI to the SPA — the "fully wired replacement" milestone. **Do** - Make the SPA the default UI (root route); redirect legacy Blazor page routes. - Remove Blazor pages/components as their SPA equivalents ship; drop MudBlazor deps once nothing depends on them. - Update docs (`CLAUDE.md`, `docs/`) and Docker/host config as needed (infra changes → server-management). **Acceptance:** old Blazor admin UI fully replaced by the ChicoryTV SPA; no user-facing MudBlazor screens remain. Blocked by all screens (Dashboard, Channels, EPG, Schedule, Playouts, Libraries, Channel Builder).
timothy added the frontend label 2026-07-02 01:07:19 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:42 +02:00
timothy added a new dependency 2026-07-02 01:08:43 +02:00
Author
Owner

Starting work (branch feat/91-cutover off main b564545f, post-v26.4.0). Plan: recon first, then staged into two PRs — (a) root-route flip to the SPA + 301 redirects for legacy Blazor routes, (b) full Blazor/MudBlazor removal. Recon will decide whether any Blazor-only functionality needs a legacy escape hatch or a blocking sub-issue.

Starting work (branch `feat/91-cutover` off main b564545f, post-v26.4.0). Plan: recon first, then staged into two PRs — (a) root-route flip to the SPA + 301 redirects for legacy Blazor routes, (b) full Blazor/MudBlazor removal. Recon will decide whether any Blazor-only functionality needs a legacy escape hatch or a blocking sub-issue.
Author
Owner

Recon complete — staging decision.

The SPA covers 8 screen areas, but the Blazor UI still has ~55 routes with no SPA equivalent: collections editing (/app/collections is a placeholder), media browsing/detail/search/trash, Trakt lists, filler presets, watermarks, the full FFmpeg profile editor, blocks/decos/templates/deco-templates, playout detail editors, logs, and troubleshooting. Removing Blazor today would delete those features.

Plan:

  • PR (a) now — root flip + redirects: / 302→/app; legacy routes with SPA equivalents (channels/schedules/playouts/libraries/settings/*) 302 to their SPA counterparts; Blazor-only routes stay served by Blazor (/system/health remains the Blazor home escape hatch). 302 not 301 — the map evolves while parity lands, and permanent-redirect caching would make rollback painful. The SPA keeps its /app mount (Vite base: '/app/' hardcodes asset URLs — serving it at root would 404 all assets).
  • PR (b) — Blazor/MudBlazor removal is gated on parity: filed blocking sub-issues #140–#146. #91 stays open until those land and Blazor is actually removed.

Docker smoke test hits / and tolerates a redirect; PR (a) also adds an explicit /app smoke assertion.

**Recon complete — staging decision.** The SPA covers 8 screen areas, but the Blazor UI still has ~55 routes with **no SPA equivalent**: collections editing (`/app/collections` is a placeholder), media browsing/detail/search/trash, Trakt lists, filler presets, watermarks, the full FFmpeg profile editor, blocks/decos/templates/deco-templates, playout detail editors, logs, and troubleshooting. Removing Blazor today would delete those features. **Plan:** - **PR (a) now — root flip + redirects**: `/` 302→`/app`; legacy routes *with* SPA equivalents (channels/schedules/playouts/libraries/settings/*) 302 to their SPA counterparts; Blazor-only routes stay served by Blazor (`/system/health` remains the Blazor home escape hatch). 302 not 301 — the map evolves while parity lands, and permanent-redirect caching would make rollback painful. The SPA keeps its `/app` mount (Vite `base: '/app/'` hardcodes asset URLs — serving it at root would 404 all assets). - **PR (b) — Blazor/MudBlazor removal** is gated on parity: filed blocking sub-issues #140–#146. #91 stays open until those land and Blazor is actually removed. Docker smoke test hits `/` and tolerates a redirect; PR (a) also adds an explicit `/app` smoke assertion.
Author
Owner

Phase (a) merged — PR #148, main d04769cc. The ChicoryTV SPA is now the default UI: / and 12 legacy routes with SPA equivalents 302 to /app/... (query strings + ETV_BASE_URL preserved; 302 deliberately, not 301). Blazor keeps serving the un-migrated screens; Blazor home = /system/health.

What was done: ErsatzTV/LegacyUiRedirects.cs (exact-match redirect map — the single source of truth for what has migrated) + middleware in Startup's Blazor branch before UseRouting; 32 new tests (ErsatzTV.Tests now 527); docker smoke now asserts /app/ serves the SPA. Verified live against a fresh server (redirects, Blazor-only routes, /iptv, /api, /docs, SPA client-route fallback), reviewed by 3 lenses + a verification fork (found+fixed: PathBase must be re-applied to redirect targets since UsePathBase never rewrites Location headers).

Remaining for phase (b) (Blazor/MudBlazor deletion, closes this issue): blocked on SPA parity #140–#146 (+#147 escape-hatch link). Known gaps flagged during review: OIDC installs now land on the open SPA instead of a challenged Blazor page (no new exposure — GET APIs and /app were already unauthenticated — but SPA auth is a phase-(b) design item); the SPA Channels edit pencil is a dead end (noted on #146); PlayoutsScreen can't reach playout editors (noted on #144).

Docs updated inline: CLAUDE.md architecture/conventions; homelab-docs Docker/ErsatzTV.md architecture section.

**Phase (a) merged** — PR #148, main d04769cc. The ChicoryTV SPA is now the default UI: `/` and 12 legacy routes with SPA equivalents 302 to `/app/...` (query strings + ETV_BASE_URL preserved; 302 deliberately, not 301). Blazor keeps serving the un-migrated screens; Blazor home = `/system/health`. **What was done:** `ErsatzTV/LegacyUiRedirects.cs` (exact-match redirect map — the single source of truth for what has migrated) + middleware in Startup's Blazor branch before UseRouting; 32 new tests (ErsatzTV.Tests now 527); docker smoke now asserts `/app/` serves the SPA. Verified live against a fresh server (redirects, Blazor-only routes, /iptv, /api, /docs, SPA client-route fallback), reviewed by 3 lenses + a verification fork (found+fixed: PathBase must be re-applied to redirect targets since UsePathBase never rewrites Location headers). **Remaining for phase (b)** (Blazor/MudBlazor deletion, closes this issue): blocked on SPA parity #140–#146 (+#147 escape-hatch link). Known gaps flagged during review: OIDC installs now land on the open SPA instead of a challenged Blazor page (no new exposure — GET APIs and /app were already unauthenticated — but SPA auth is a phase-(b) design item); the SPA Channels edit pencil is a dead end (noted on #146); PlayoutsScreen can't reach playout editors (noted on #144). Docs updated inline: CLAUDE.md architecture/conventions; homelab-docs Docker/ErsatzTV.md architecture section.
Author
Owner

Phase (b) readiness check (2026-07-07, post-#180/#182/#183)

Full route reconciliation now lives in docs/blazor-route-parity.md (landing on main with this session's docs commit; keep it updated per PR). Summary of the 124 Blazor @page routes:

  • 13 redirected (phase a, unchanged).
  • ~105 SPA-READY but not redirected — the SPA equivalent exists (scheduling editors from #144/#162, troubleshooting from #145/PR #182, media browse/detail/search/trash/image browser from #141/PR #183, plus channels/schedules/collections/filler/watermarks/ffmpeg/sources/trakt) but LegacyUiRedirects.cs has no entry yet.
  • BLAZOR-ONLY remaining — the actual phase (b) gates, 4 areas:
    1. /system/troubleshooting/playback → #145 (last item there; HLS player + session logs; L, own session).
    2. Multi-collections (list/add/edit) → #151 (no REST endpoints).
    3. Rerun collections (list/add/edit) → #152 (no REST endpoints).
    4. Playlists editing (/media/playlists/{id}) + collection items listing → #153/#155 (S3's PlaylistController is GET-only list/groups; SPA collections screen must reach Blazor PlaylistEditor/CollectionItems depth before these routes can die — verify SPA editing depth as part of #153/#155, don't assume).

/system/health (Blazor home) needs no successor: the SPA dashboard's health panel covers it, and the Settings→System "Classic UI" link gets removed with Blazor.

Removal plan (execute when the 4 gates close)

Step 1 — redirects sweep [S]: add every SPA-READY route to LegacyUiRedirects.cs (including id-carrying detail/editor routes → their /app/... equivalents). Ship separately from deletion; lets stragglers surface as harmless 302s while Blazor still exists as fallback.

Step 2 — Blazor deletion [M-L, one session]: remove ErsatzTV/Pages/, ErsatzTV/Shared/, Blazor-only ErsatzTV/ViewModels/ (e.g. ImageTreeItemViewModel), _Host.cshtml/App.razor/MainLayout.razor; strip Blazor Server DI + endpoints from startup (AddServerSideBlazor, MapBlazorHub, circuits); drop MudBlazor + Blazor-only static assets (jQuery/jQueryUI/Sortable) and prune their Directory.Packages.props entries. No SignalR hubs outside Blazor (verified). KEEP: all MVC/API controllers (ArtworkController etc. serve the SPA), all Application VMs (handlers own them; pruning Blazor-only VMs is a later cleanup).

Step 3 — verification + docs [S]: full gates (build, all test projects incl. Architecture, web test/lint/build) + a live-E2E redirect sweep (every legacy route 302s correctly, /app/* unaffected, /iptv/* + /artwork/* untouched) + Docker image build check. Update CLAUDE.md architecture section, docs/blazor-route-parity.md (all rows → redirected), docs/contributing.md Blazor/MudBlazor sections, Obsidian ErsatzTV doc.

Suggested order: #185 docs part 2 + quick wins → #155 (smallest, unblocks collections depth) → #151/#152 (mirror each other; one session) → #153 → #145 playback (own session) → Step 1 → Step 2+3. Step 1 (redirect sweep for already-covered routes) could also ship earlier at any point — it's independent of the gates.

## Phase (b) readiness check (2026-07-07, post-#180/#182/#183) Full route reconciliation now lives in **`docs/blazor-route-parity.md`** (landing on main with this session's docs commit; keep it updated per PR). Summary of the 124 Blazor `@page` routes: - **13 redirected** (phase a, unchanged). - **~105 SPA-READY but not redirected** — the SPA equivalent exists (scheduling editors from #144/#162, troubleshooting from #145/PR #182, media browse/detail/search/trash/image browser from #141/PR #183, plus channels/schedules/collections/filler/watermarks/ffmpeg/sources/trakt) but `LegacyUiRedirects.cs` has no entry yet. - **BLAZOR-ONLY remaining — the actual phase (b) gates, 4 areas**: 1. `/system/troubleshooting/playback` → **#145** (last item there; HLS player + session logs; L, own session). 2. Multi-collections (list/add/edit) → **#151** (no REST endpoints). 3. Rerun collections (list/add/edit) → **#152** (no REST endpoints). 4. Playlists editing (`/media/playlists/{id}`) + collection items listing → **#153/#155** (S3's PlaylistController is GET-only list/groups; SPA collections screen must reach Blazor `PlaylistEditor`/`CollectionItems` depth before these routes can die — verify SPA editing depth as part of #153/#155, don't assume). `/system/health` (Blazor home) needs no successor: the SPA dashboard's health panel covers it, and the Settings→System "Classic UI" link gets removed with Blazor. ## Removal plan (execute when the 4 gates close) **Step 1 — redirects sweep [S]**: add every SPA-READY route to `LegacyUiRedirects.cs` (including id-carrying detail/editor routes → their `/app/...` equivalents). Ship separately from deletion; lets stragglers surface as harmless 302s while Blazor still exists as fallback. **Step 2 — Blazor deletion [M-L, one session]**: remove `ErsatzTV/Pages/`, `ErsatzTV/Shared/`, Blazor-only `ErsatzTV/ViewModels/` (e.g. `ImageTreeItemViewModel`), `_Host.cshtml`/`App.razor`/`MainLayout.razor`; strip Blazor Server DI + endpoints from startup (`AddServerSideBlazor`, `MapBlazorHub`, circuits); drop MudBlazor + Blazor-only static assets (jQuery/jQueryUI/Sortable) and prune their `Directory.Packages.props` entries. No SignalR hubs outside Blazor (verified). KEEP: all MVC/API controllers (`ArtworkController` etc. serve the SPA), all Application VMs (handlers own them; pruning Blazor-only VMs is a later cleanup). **Step 3 — verification + docs [S]**: full gates (build, all test projects incl. Architecture, web test/lint/build) + a live-E2E redirect sweep (every legacy route 302s correctly, /app/* unaffected, /iptv/* + /artwork/* untouched) + Docker image build check. Update CLAUDE.md architecture section, `docs/blazor-route-parity.md` (all rows → redirected), `docs/contributing.md` Blazor/MudBlazor sections, Obsidian ErsatzTV doc. **Suggested order**: #185 docs part 2 + quick wins → #155 (smallest, unblocks collections depth) → #151/#152 (mirror each other; one session) → #153 → #145 playback (own session) → Step 1 → Step 2+3. Step 1 (redirect sweep for already-covered routes) could also ship earlier at any point — it's independent of the gates.
Author
Owner

Gate added for phase (b) — cold adversarial review required BEFORE the Blazor-removal PR.

Every parity claim to date (docs/blazor-route-parity.md, the per-screen E2E passes) was produced inside the same session workflow that built the SPA — shared-context reviews can inherit shared blind spots. Since deleting Blazor removes both the reference implementation and the escape hatch, phase (b) must start with a zero-shared-context review:

  1. From ~/adversarial-reviewer, run /review in pre-implementation plan review mode against the phase (b) removal plan.
  2. Brief for the reviewer (give it NOTHING else from our sessions): "ersatztv is retiring its legacy Blazor UI in favor of the React SPA in web/. Verify independently that nothing of value dies: walk ErsatzTV/Pages/**/*.razor and confirm every user-facing capability has a working SPA equivalent (docs/blazor-route-parity.md is the claim — verify it, don't trust it), check for Blazor-only behaviors not visible as routes (JS interop, SignalR usage, background triggers fired from pages), and review the removal plan for blast radius/rollback (can we restore Blazor from a tag if a gap surfaces post-deletion?)."
  3. Findings get filed via /dispatch (review label); phase (b) implementation starts only after MUST-FIX findings are resolved or explicitly waived.

Do not close this issue without this review having happened.

**Gate added for phase (b) — cold adversarial review required BEFORE the Blazor-removal PR.** Every parity claim to date (docs/blazor-route-parity.md, the per-screen E2E passes) was produced inside the same session workflow that built the SPA — shared-context reviews can inherit shared blind spots. Since deleting Blazor removes both the reference implementation and the escape hatch, phase (b) must start with a zero-shared-context review: 1. From `~/adversarial-reviewer`, run `/review` in **pre-implementation plan review** mode against the phase (b) removal plan. 2. Brief for the reviewer (give it NOTHING else from our sessions): "ersatztv is retiring its legacy Blazor UI in favor of the React SPA in `web/`. Verify independently that nothing of value dies: walk `ErsatzTV/Pages/**/*.razor` and confirm every user-facing capability has a working SPA equivalent (docs/blazor-route-parity.md is the *claim* — verify it, don't trust it), check for Blazor-only behaviors not visible as routes (JS interop, SignalR usage, background triggers fired from pages), and review the removal plan for blast radius/rollback (can we restore Blazor from a tag if a gap surfaces post-deletion?)." 3. Findings get filed via `/dispatch` (review label); phase (b) implementation starts only after MUST-FIX findings are resolved or explicitly waived. Do not close this issue without this review having happened.
Author
Owner

Cold adversarial review gate: RUN — verdict GATE: BLOCKED (3 must-fix).

The review ran 2026-07-09 from the external ~/adversarial-reviewer project as a fresh zero-shared-context session (headless, given only the brief from the gate comment above + factual pointers). Findings filed via /dispatch as review-labeled issues:

  • #202 MUST-FIX — media-source add/edit/path-replacement/library-select has no SPA equivalent and no write REST API (MediaSourcesController is GET-only; SPA "Add Source" is a hardcoded disabled stub). Deleting Blazor would make a fresh install unable to connect a library. Verified in code (App.tsx:2557, single [HttpGet] in MediaSourcesController).
  • #203 MUST-FIX — docs/blazor-route-parity.md's "SPA-READY" conflated screen-exists with functional parity; every Section-2 row needs mutation-depth verification. A systematic per-cluster verification sweep is running now; results + any promoted sub-issues will be posted on #203.
  • #204 MUST-FIX — LegacyUiRedirects is exact-match only; the Step-1 promise to redirect id-carrying routes is infeasible as written. Verified (dictionary TryGetValue). Resolution = pattern/prefix redirect support in the removal PR; strategy to be posted on #204.
  • #205 SHOULD-FIX — cut a blazor-final rollback tag immediately before the removal merge + document restore.
  • #206 SHOULD-FIX — confirm auth posture (OIDC challenged Blazor pages disappear) before deletion.

What the cold review confirmed safe: no SignalR outside MapBlazorHub, JS interop is cosmetic-only, SmartCollection editing has real parity.

Phase (b) implementation stays blocked until #202/#203/#204 are resolved. #202 is the new feature gate (peer of #151/#152/#153) and gets its own session.

**Cold adversarial review gate: RUN — verdict GATE: BLOCKED (3 must-fix).** The review ran 2026-07-09 from the external `~/adversarial-reviewer` project as a fresh zero-shared-context session (headless, given only the brief from the gate comment above + factual pointers). Findings filed via `/dispatch` as `review`-labeled issues: - **#202 MUST-FIX** — media-source add/edit/path-replacement/library-select has no SPA equivalent and no write REST API (`MediaSourcesController` is GET-only; SPA "Add Source" is a hardcoded disabled stub). Deleting Blazor would make a fresh install unable to connect a library. **Verified in code** (App.tsx:2557, single `[HttpGet]` in MediaSourcesController). - **#203 MUST-FIX** — `docs/blazor-route-parity.md`'s "SPA-READY" conflated screen-exists with functional parity; every Section-2 row needs mutation-depth verification. **A systematic per-cluster verification sweep is running now**; results + any promoted sub-issues will be posted on #203. - **#204 MUST-FIX** — `LegacyUiRedirects` is exact-match only; the Step-1 promise to redirect id-carrying routes is infeasible as written. **Verified** (dictionary `TryGetValue`). Resolution = pattern/prefix redirect support in the removal PR; strategy to be posted on #204. - **#205 SHOULD-FIX** — cut a `blazor-final` rollback tag immediately before the removal merge + document restore. - **#206 SHOULD-FIX** — confirm auth posture (OIDC challenged Blazor pages disappear) before deletion. What the cold review confirmed safe: no SignalR outside `MapBlazorHub`, JS interop is cosmetic-only, SmartCollection editing has real parity. Phase (b) implementation stays blocked until #202/#203/#204 are resolved. #202 is the new feature gate (peer of #151/#152/#153) and gets its own session.
Author
Owner

Phase (b) gate — consolidated status after resolving #203 (mutation-depth sweep complete).

The definitive blocker list for Blazor removal is now:

MUST-FIX feature gates (build before removal):

  1. #202 media-source management (write API + SPA) — largest; own session.
  2. #207 schedules editor mutation depth (create/edit/delete + ~35 per-item fields; endpoints exist) — large; own session.
  3. #208 search screen mutations (select, add-to-collection/playlist, save-as-smart, card nav) + #209 media browse/detail mutations (shared "Add to…" component; per-show scan wiring; episode info/troubleshoot entries) — one or two sessions.
  4. #210 playouts (wire existing delete/reset endpoints; new erase-items/history + scheduling-context endpoints) — medium.
  5. #211 collections (custom-order endpoint + UI; add-items all 10 kinds) — small-medium.

SHOULD-FIX before/with removal: #212 (channel editor), #213 (nits batch + decision-log entries).

Resolved from the cold review: #203 closed (sweep done, docs/blazor-route-parity.md corrected — verdict table embedded, schedules + media-sources rows moved to Section 3); #204 has an agreed pattern-redirect + catch-all strategy (lands in the removal PR); #205/#206 planned into the removal PR routine.

Removal itself (already reconned: Courier is Blazor-only-consumed; 6 packages removable; the Startup MapWhen branch co-hosts the REST API and needs surgical—not wholesale—removal) starts only after 1–5 close.

**Phase (b) gate — consolidated status after resolving #203 (mutation-depth sweep complete).** The definitive blocker list for Blazor removal is now: **MUST-FIX feature gates (build before removal):** 1. **#202** media-source management (write API + SPA) — largest; own session. 2. **#207** schedules editor mutation depth (create/edit/delete + ~35 per-item fields; endpoints exist) — large; own session. 3. **#208** search screen mutations (select, add-to-collection/playlist, save-as-smart, card nav) + **#209** media browse/detail mutations (shared "Add to…" component; per-show scan wiring; episode info/troubleshoot entries) — one or two sessions. 4. **#210** playouts (wire existing delete/reset endpoints; new erase-items/history + scheduling-context endpoints) — medium. 5. **#211** collections (custom-order endpoint + UI; add-items all 10 kinds) — small-medium. **SHOULD-FIX before/with removal:** #212 (channel editor), #213 (nits batch + decision-log entries). **Resolved from the cold review:** #203 closed (sweep done, `docs/blazor-route-parity.md` corrected — verdict table embedded, schedules + media-sources rows moved to Section 3); #204 has an agreed pattern-redirect + catch-all strategy (lands in the removal PR); #205/#206 planned into the removal PR routine. Removal itself (already reconned: Courier is Blazor-only-consumed; 6 packages removable; the Startup `MapWhen` branch co-hosts the REST API and needs surgical—not wholesale—removal) starts only after 1–5 close.
timothy added this to the Blazor removal (#91 phase b) milestone 2026-07-11 01:14:37 +02:00
timothy added the enhancementpriority: high labels 2026-07-11 19:03:42 +02:00
timothy added the in-progress label 2026-07-11 19:16:34 +02:00
Author
Owner

claiming — session started 2026-07-11 (evening/late).

Working the last SPA pre-work gate for the #91b removal PR (per blazor-route-parity.md §5): wiring the already-shipped scanLibrary(id, deep) + scanCollections(family, id, deep) clients into LibrariesScreen as Deep Scan Library and External Collections (quick + deep) affordances, matching Libraries.razor's four scan actions. This is a pure-SPA change — the F9 API + clients already landed (#235), and the External Collections rows derive faithfully from the existing getMediaSources() (the API handler already filters each source's libraries to sync-enabled, so a remote source appears iff libraries.length > 0 — exactly GetExternalCollections's Libraries.Any(ShouldSyncItems) filter). No backend/OpenAPI change.

The removal PR proper (rollback tag → surgical Blazor delete + catch-all fallback → auth cleanup → verify, with the mandatory cold adversarial pass + Fable go/no-go) remains a separate focused follow-up; this session only lands the capability so Libraries.razor can be deleted at parity.

claiming — session started 2026-07-11 (evening/late). Working the **last SPA pre-work gate** for the #91b removal PR (per `blazor-route-parity.md §5`): wiring the already-shipped `scanLibrary(id, deep)` + `scanCollections(family, id, deep)` clients into `LibrariesScreen` as **Deep Scan Library** and **External Collections** (quick + deep) affordances, matching `Libraries.razor`'s four scan actions. This is a pure-SPA change — the F9 API + clients already landed (#235), and the External Collections rows derive faithfully from the existing `getMediaSources()` (the API handler already filters each source's `libraries` to sync-enabled, so a remote source appears iff `libraries.length > 0` — exactly `GetExternalCollections`'s `Libraries.Any(ShouldSyncItems)` filter). No backend/OpenAPI change. The removal PR proper (rollback tag → surgical Blazor delete + catch-all fallback → auth cleanup → verify, with the mandatory cold adversarial pass + Fable go/no-go) remains a separate focused follow-up; this session only lands the capability so `Libraries.razor` can be deleted at parity.
timothy removed the in-progress label 2026-07-11 19:34:43 +02:00
Author
Owner

Progress (SPA pre-work landed — #91 stays open for the removal PR proper).

PR #272 merged to main (bb7b1899, fast-forward): LibrariesScreen now reaches parity with Libraries.razor's four scan actions — Deep Scan Library on remote library rows + an External Collections section (quick + deep per remote source). This closes the last SPA pre-work gate for the Blazor removal; per blazor-route-parity.md §5 the removal PR's only remaining pre-work is now the mandatory cold adversarial pass (the #204 catch-all fallback lands in the removal PR itself). No API gate remains (F9 closed via #235).

Verification: CI green; adversarial review fork = MERGEABLE (no Blocker/High/Medium); live headless E2E = PASS against a real DB/handler, including a negative control (a ShouldSyncItems=0 library correctly excluded from both the library rows and the External Collections derivation).

Approach note: External Collections rows derive client-side from getMediaSources() (no new endpoint) — the media-sources API handler already filters each source's libraries to sync-enabled entries, exactly matching GetExternalCollections's Libraries.Any(ShouldSyncItems). Collections scans have no status/progress REST surface, so their button state is optimistic + timeout-bounded — follow-up #271 filed for a proper collections status endpoint.

Docs updated in-PR: blazor-route-parity.md §5, decisions.md.

**Progress (SPA pre-work landed — #91 stays open for the removal PR proper).** PR **#272** merged to main (`bb7b1899`, fast-forward): `LibrariesScreen` now reaches parity with `Libraries.razor`'s four scan actions — **Deep Scan Library** on remote library rows + an **External Collections** section (quick + deep per remote source). This closes the last SPA pre-work gate for the Blazor removal; per `blazor-route-parity.md §5` the removal PR's only remaining pre-work is now the **mandatory cold adversarial pass** (the #204 catch-all fallback lands in the removal PR itself). No API gate remains (F9 closed via #235). **Verification:** CI green; adversarial review fork = MERGEABLE (no Blocker/High/Medium); live headless E2E = PASS against a real DB/handler, including a negative control (a `ShouldSyncItems=0` library correctly excluded from both the library rows and the External Collections derivation). **Approach note:** External Collections rows derive client-side from `getMediaSources()` (no new endpoint) — the media-sources API handler already filters each source's `libraries` to sync-enabled entries, exactly matching `GetExternalCollections`'s `Libraries.Any(ShouldSyncItems)`. Collections scans have no status/progress REST surface, so their button state is optimistic + timeout-bounded — follow-up **#271** filed for a proper collections status endpoint. Docs updated in-PR: `blazor-route-parity.md §5`, `decisions.md`.
timothy added the in-progress label 2026-07-11 19:49:26 +02:00
Author
Owner

claiming — session started 2026-07-11 (late evening). Picking up arc item 4: the #91 phase (b) Blazor-removal PR. All gates cleared per the tracker (#237): #202 media-source write API+SPA DONE, #235 F9 deep-scan/external-collections API DONE, SPA deep-scan/collections buttons DONE (PR #272). Remaining pre-work per blazor-route-parity.md §5: mandatory cold adversarial pass + Fable go/no-go, then the 4-step removal runbook (rollback tag blazor-final → surgical Blazor delete + #204 catch-all fallback → auth-attached-OIDC cleanup → verify+docs).

Confirmed no active session is mid-flight on a Blazor page: the live parallel worktrees (#253 PR2 #268, PR3) touch versioned aggregate handlers + SPA editors, not ErsatzTV/Pages/** or the Startup.cs Blazor MapWhen branch. Will keep the deletion diff surgical and preserve the co-hosted MapControllers()//docs/redirect-middleware.

claiming — session started 2026-07-11 (late evening). Picking up **arc item 4: the #91 phase (b) Blazor-removal PR**. All gates cleared per the tracker (#237): #202 media-source write API+SPA DONE, #235 F9 deep-scan/external-collections API DONE, SPA deep-scan/collections buttons DONE (PR #272). Remaining pre-work per `blazor-route-parity.md §5`: mandatory cold adversarial pass + Fable go/no-go, then the 4-step removal runbook (rollback tag `blazor-final` → surgical Blazor delete + #204 catch-all fallback → auth-attached-OIDC cleanup → verify+docs). Confirmed no active session is mid-flight on a Blazor page: the live parallel worktrees (#253 PR2 #268, PR3) touch versioned aggregate handlers + SPA editors, not `ErsatzTV/Pages/**` or the `Startup.cs` Blazor `MapWhen` branch. Will keep the deletion diff surgical and preserve the co-hosted `MapControllers()`/`/docs`/redirect-middleware.
Author
Owner

Done — legacy Blazor Server UI removed (PR #274, merged FF to main 78262ea4)

The ChicoryTV React SPA (web/, served at /app) is now the only UI. This closes #91 phase (b) — the milestone-capping removal.

What was done (runbook: docs/blazor-route-parity.md §5, executed in order):

  1. Rollback tag blazor-final cut on the pre-deletion main commit bb310f6b (annotated; not a v* tag → no prod release build). Restore = git checkout blazor-final (test image) or git revert the merge.
  2. Surgical Blazor delete (207 files): ErsatzTV/Pages/**, Shared/**, ViewModels/** (39), Validators/** (10), App.razor, _Imports.razor, Locals/{Shared,Pages}/** (Blazor loc resx; Locals/Resources.* kept), wwwroot/css + wwwroot/lib, libman.json, an orphaned NavigationManagerExtensions.cs, MultiSelectBaseTests.cs. Startup.cs reduced: dropped AddRazorPages/AddServerSideBlazor/AddMudServices/AddSortable/AddCourier/IHtmlSanitizer, the Blazor-attached OIDC middleware, MapBlazorHub, MapFallbackToPage("/_Host"); renamed the branch blazor→legacy; added the #204 catch-all (MapFallback→302 /app, excluding /api|/artwork|/docs|/openapi→404). Pruned 9 now-dead packages + the dead #25 razor-Sonar NoWarn. Added the 14 /media/sources/*→/app/libraries/* redirects + lifted the #204 prefix ban.
  3. Auth posture per #206: only the Blazor-attached OIDC pieces removed; OIDC/JWT/API-key service wiring, ConditionalIptvAuthorizeFilter (/iptv), ApiKeyAuthorizationFilter (mutating /api) all kept. Real SPA/API auth remains #197.
  4. Verify + docs: full dotnet test ErsatzTV.sln green on the merged tree (ErsatzTV.Tests 1404, Core 493, Scanner 1467, Infra 53, Arch 5, FFmpeg 20; 0 failed); PR CI green (Build & test, EF migration integrity, Docs reminder, image build). Docs updated in-PR: blazor-route-parity.md (phase b COMPLETE), decisions.md, CLAUDE.md, contributing.md, README.md.

Verification gates cleared: 2 independent adversarial reviews (cold fork + Codex, both MERGEABLE-WITH-NITS, no disagreement — all nits fixed), a live-E2E redirect sweep (20/20: legacy exact/pattern/media-sources routes 302 correctly, catch-all→/app, /api 404 / /artwork 400 never redirected, no 500s), and a Fable GO go/no-go.

Deferred / residual (accepted, non-blocking): non-GET requests to retired routes 302→/app rather than 404/405 (same class as the old _Host fallback; mutating /api still 404s); bookmarked /system/health lands on /app (SPA sidebar health dot replaces it); real SPA/API auth stays open until #197. ~30 historical "matches Blazor's behavior" rationale comments left as accurate documentation (only references to deleted code were cleaned).

Milestone "Blazor removal (#91 phase b)" is now complete. Root / and every legacy route reach the SPA (redirect map + catch-all).

## Done — legacy Blazor Server UI removed (PR #274, merged FF to main `78262ea4`) The ChicoryTV React SPA (`web/`, served at `/app`) is now the **only** UI. This closes #91 phase (b) — the milestone-capping removal. **What was done** (runbook: `docs/blazor-route-parity.md §5`, executed in order): 1. **Rollback tag `blazor-final`** cut on the pre-deletion `main` commit `bb310f6b` (annotated; not a `v*` tag → no prod release build). Restore = `git checkout blazor-final` (test image) or `git revert` the merge. 2. **Surgical Blazor delete** (207 files): `ErsatzTV/Pages/**`, `Shared/**`, `ViewModels/**` (39), `Validators/**` (10), `App.razor`, `_Imports.razor`, `Locals/{Shared,Pages}/**` (Blazor loc resx; `Locals/Resources.*` kept), `wwwroot/css` + `wwwroot/lib`, `libman.json`, an orphaned `NavigationManagerExtensions.cs`, `MultiSelectBaseTests.cs`. `Startup.cs` reduced: dropped `AddRazorPages`/`AddServerSideBlazor`/`AddMudServices`/`AddSortable`/`AddCourier`/`IHtmlSanitizer`, the Blazor-attached OIDC middleware, `MapBlazorHub`, `MapFallbackToPage("/_Host")`; renamed the branch `blazor`→`legacy`; added the **#204 catch-all** (`MapFallback`→302 `/app`, excluding `/api|/artwork|/docs|/openapi`→404). Pruned 9 now-dead packages + the dead #25 razor-Sonar NoWarn. Added the 14 `/media/sources/*`→`/app/libraries/*` redirects + lifted the #204 prefix ban. 3. **Auth posture** per #206: only the Blazor-attached OIDC pieces removed; OIDC/JWT/API-key service wiring, `ConditionalIptvAuthorizeFilter` (`/iptv`), `ApiKeyAuthorizationFilter` (mutating `/api`) all **kept**. Real SPA/API auth remains #197. 4. **Verify + docs**: full `dotnet test ErsatzTV.sln` green on the merged tree (ErsatzTV.Tests 1404, Core 493, Scanner 1467, Infra 53, Arch 5, FFmpeg 20; 0 failed); PR CI green (Build & test, EF migration integrity, Docs reminder, image build). Docs updated in-PR: `blazor-route-parity.md` (phase b COMPLETE), `decisions.md`, `CLAUDE.md`, `contributing.md`, `README.md`. **Verification gates cleared**: 2 independent adversarial reviews (cold fork + Codex, both MERGEABLE-WITH-NITS, no disagreement — all nits fixed), a live-E2E redirect sweep (20/20: legacy exact/pattern/media-sources routes 302 correctly, catch-all→/app, `/api` 404 / `/artwork` 400 never redirected, no 500s), and a **Fable GO** go/no-go. **Deferred / residual (accepted, non-blocking)**: non-GET requests to retired routes 302→`/app` rather than 404/405 (same class as the old `_Host` fallback; mutating `/api` still 404s); bookmarked `/system/health` lands on `/app` (SPA sidebar health dot replaces it); real SPA/API auth stays open until **#197**. ~30 historical "matches Blazor's behavior" rationale comments left as accurate documentation (only references to *deleted code* were cleaned). Milestone "Blazor removal (#91 phase b)" is now complete. Root `/` and every legacy route reach the SPA (redirect map + catch-all).
timothy removed the in-progress label 2026-07-11 21:31:25 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: timothy/ersatztv#91