diff --git a/docs/blazor-route-parity.md b/docs/blazor-route-parity.md index d36685835..fec77305b 100644 --- a/docs/blazor-route-parity.md +++ b/docs/blazor-route-parity.md @@ -240,6 +240,35 @@ intentional exit ramp until phase (b) removes Blazor entirely. |---|---|---| | `/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), +#235 F9 (deep-scan + external-collections-scan API — no API today, so `Libraries.razor` can't be deleted +yet), and the **mandatory cold adversarial pass** (#91 comment 2026-07-09). (#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: + +1. **Cut the rollback tag `blazor-final`** on the pre-deletion `main` commit — the *first* action, before + deleting anything. Exact command + restore path: `docs/decisions.md` 2026-07-11 "Pre-removal Blazor + rollback tag `blazor-final`" (#205). Not a `v*` tag → does not trigger a prod release build. +2. **Delete Blazor** per #91's Step 2 recon (comment 2026-07-07): `ErsatzTV/Pages/`, `Shared/`, `App.razor`, + Blazor-only `ViewModels/`, `_Host.cshtml`; strip `AddServerSideBlazor`/`MapBlazorHub`/circuits + MudBlazor + + jQuery/jQueryUI/Sortable and prune their `Directory.Packages.props` entries. **Surgical, not wholesale**, on + the Blazor `MapWhen` branch (`Startup.cs:736-795`): it co-hosts `MapControllers()` + `/docs` (Scalar) + the + legacy-redirect middleware, all of which must survive. Replace `MapFallbackToPage("/_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 `_Host` is gone. +3. **Auth posture is already signed off** — no separate auth step beyond deleting the Blazor-attached OIDC + pieces (`AuthorizeFolder("/")` via `AddRazorPages`, `blazor.UseAuthentication/UseAuthorization`). **Keep** the + OIDC/JWT/API-key service wiring (independent gates: `ConditionalIptvAuthorizeFilter` on `/iptv/*`, + `ApiKeyAuthorizationFilter` on mutating `/api/*`). Full analysis + must-not-break list: + `docs/decisions.md` 2026-07-11 "Blazor removal auth posture" (#206). Real SPA/API auth = **#197**. +4. **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.md` Blazor/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 diff --git a/docs/decisions.md b/docs/decisions.md index 143c07c71..1a9c1f7d7 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -500,3 +500,66 @@ tests, no benefit — every parameterized route here is "fixed segments + one va ASP.NET `TemplateMatcher`/`RouteMatcher` (pulls routing machinery into a static helper for 36 rules); a single unified rule list (loses the O(1) dictionary hit for the ~52 exact routes that dominate real traffic). No `/api` change, no OpenAPI regen, no SPA change. + +## 2026-07-11 — Blazor removal auth posture: no new exposure beyond phase (a); real auth deferred to #197 (#206) + +Sign-off for the #91 phase (b) removal-gate item #206 ("deleting the last challenged Blazor page leaves +only the open SPA"). The actual authorization wiring in `ErsatzTV/Startup.cs` + `ErsatzTV/Pages` was +enumerated in code (not assumed) before clearing the gate. + +**What is gated today** +- **OIDC** (`OidcHelper.IsEnabled` — active only when `Authority`/`ClientId`/`ClientSecret` are configured): + `AddAuthentication` (cookie default, `oidc` challenge) + `AddAuthorization` `DefaultPolicy = + RequireAuthenticatedUser` + **`AddRazorPages(… AuthorizeFolder("/"))`** (Startup.cs:379-385) + + `blazor.UseAuthentication()/UseAuthorization()` inside the Blazor `MapWhen` branch (Startup.cs:764-770). + `AuthorizeFolder("/")` gates **Razor Pages only**, and the sole user-facing Razor Page is + `Pages/_Host.cshtml` — the Blazor Server host (the other `.cshtml`, `Shared/_Favicons.cshtml`, is a + cosmetic partial). **So the OIDC challenge protects exactly the Blazor UI and nothing else.** +- **`/app` (SPA)** is served by its own `MapWhen(path=/app)` static-file branch (Startup.cs:701-714) with + **no authentication/authorization middleware** — open since phase (a) (`/`→`/app`, PR #148). +- **`/api/*` controllers** carry no `[Authorize]` (verified: zero attributes in `Controllers/`); the + Razor-Pages `AuthorizeFolder`/`DefaultPolicy` never reach them. Their only optional gate is the + per-endpoint `ApiKeyAuthorizationFilter` (API-key on mutating JSON endpoints), independent of OIDC/Blazor. +- **`/iptv/*`** is gated by `ConditionalIptvAuthorizeFilter` (JWT `JwtOnlyScheme`, active only when + `JwtHelper.IsEnabled`) in its own `MapWhen` branch (Startup.cs:797-803) — independent of Blazor. + +**Posture after Blazor removal.** Removing `Pages/_Host.cshtml`, `AddRazorPages`/`AuthorizeFolder("/")`, +`blazor.UseAuthentication/UseAuthorization`, `MapBlazorHub`, and `MapFallbackToPage("/_Host")` deletes the +OIDC challenge's **only attachment point** — no user-facing surface remains challenged. **No capability is +lost:** every Blazor-served capability already has an open SPA equivalent (the #91 parity effort), and the +SPA was already the unauthenticated path since phase (a), so removal exposes nothing a user could not already +reach via `/app`. + +**The one honest caveat (not a regression introduced by removal):** an OIDC-configured operator's *Blazor* +admin UI sits behind a login today; after removal there is no login-gated admin UI at all (the SPA admin UI +is open). That exposure delta already happened at **phase (a)** (the open SPA became the default admin +surface); removal only deletes the now-redundant challenged duplicate. Designing real SPA/API authentication +is deliberately deferred to **#197** (cold API security review — a HARD GATE before any remote exposure). + +**Removal-PR must-not-break (independent gates that survive):** `ConditionalIptvAuthorizeFilter` (`/iptv/*` +JWT), `ApiKeyAuthorizationFilter` (mutating `/api/*`), and `JwtHelper` access_token query support. **Leave +the OIDC service registrations in place** (conditional on config, inert once no Razor Page consumes them) — +ripping OIDC out is a #197 decision, not a removal-PR one. The removal PR removes only the Blazor-attached +pieces above; `MapControllers()` + `/docs` (Scalar), currently co-hosted in the Blazor `MapWhen` branch, must +survive the surgical reduction. + +## 2026-07-11 — Pre-removal Blazor rollback tag `blazor-final` (#205) + +Removal-gate item #205: the removal PR deletes both the Blazor reference implementation and the +`/system/health` escape hatch, so a post-deletion parity gap would otherwise be an archaeology exercise +(guessing which release tag still matches `main` minus Blazor). Decision + procedure, to run as the **first +action of the Step 2 deletion PR merge** (not before — `main` moves until then): + +1. On the `main` commit **immediately preceding** the removal merge (the last commit that still contains + `ErsatzTV/Pages/**`), cut an annotated tag and push it: + `git tag -a blazor-final -m "Last commit with the legacy Blazor Server UI (pre-#91-phase-b removal)"` + then `git push origin blazor-final`. The tag name is **`blazor-final`** (not `v*`) so it does **not** + trigger the `v*` prod-release build in `.gitea/workflows/docker-build.yml`. +2. **Restore path** (if a gap surfaces post-removal): `git checkout blazor-final` → `docker build -f + docker/Dockerfile -t ersatztv:blazor-final .` → pin the **test** container to that image while the gap is + fixed forward on `main`. Alternatively `git revert` the single deletion merge commit (keep the deletion as + one squash/merge commit specifically to make this a one-liner). +3. Document the tag + restore path in the removal PR body; update this entry with the tag's commit sha when + cut. + +Not cut this session — `main` still carries Blazor and will advance before the removal PR.