LegacyUiRedirects matched exact paths only (a Dictionary lookup). The removal plan's Step 1 — "add every SPA-READY route incl. id-carrying detail/editor routes → their /app/... equivalents" — was not expressible: routes like /channels/{id}, /media/movies/{id}, /playouts/{id}/templates can't be static keys. After Blazor deletion they would hard-404.
Change (two-tier matcher, signature unchanged)
Tier 1 — the exact Map, grown to 52 entries (the 13 foundational routes + the #204 parameterless (A)/(B) routes + the (E-base) browse roots whose targets carry ?kind=…).
Tier 2 — an ordered list of 36 PatternRule segment templates ((C) id-substituting, (C2) mid-path id, (D) id-dropping detail→list, (E-page) /…/page/{n} → ?kind=). First-match-wins; consulted only on a Tier-1 miss.
{id} = strict positive integer (NumberStyles.None, > 0); non-int/0/negative/overflow falls through to Blazor (never a broken /app/edit-channel/abc). This strictness is also what makes the rule set collision-free by construction.
New AppendQueryString(target, QueryString) helper &-merges the incoming query into ?kind= targets, fixing the malformed double-? (/media/movies?page=2 → /app/media?kind=movies&page=2); plain targets keep verbatim-append behavior byte-for-byte. One-line Startup.cs change; redirect stays GET/HEAD-only 302 before UseRouting().
Guard invariant (comment + Map-keys meta-test): no rule may begin with /api, /artwork, /docs, /openapi, /iptv, /app, /media/sources — rules are always full templates, never prefix wildcards.
Scope
Completes phase-(a) Step 1 for every PARITY-OK route (docs/blazor-route-parity.md §2 → §1). Deliberately out of scope: the catch-all fallback replacing MapFallbackToPage("/_Host") (coupled to actual Blazor deletion — the removal PR), /media/sources/* (#202, other session), and /system/health (kept escape hatch) — both encoded as negative tests.
Extend LegacyUiRedirects from an exact-match dictionary to a two-tier matcher:
Tier 1 keeps the exact Map (now 52 entries incl. the ?kind= browse roots),
Tier 2 adds 36 ordered segment-template PatternRules for id-carrying routes.
{id} is a strict positive integer (non-int/0/neg/overflow falls through), which
also makes the rule set collision-free by construction. New AppendQueryString
helper merges the incoming query into ?kind= targets with '&' (kills the
double-'?' bug); one-line Startup change keeps the redirect GET/HEAD-only 302
before UseRouting.
Completes phase-(a) Step 1 for every PARITY-OK route (#91 phase b); the
catch-all fallback replacing MapFallbackToPage stays with the removal PR.
/media/sources/* (#202) and /system/health remain deliberately un-redirected.
fixes#204
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fork adversarial review nit: Map_Keys_Should_Not_Begin_With_Forbidden_Prefix
covered only Tier-1 Map keys, not the Tier-2 PatternRule templates. That guard
invariant is the load-bearing protection for the un-prefix-guarded /api|/artwork|
/docs|/openapi surface, so make it self-enforcing over ALL rules — a future
prefix-violating template now fails the test instead of slipping through.
Exposes internal LegacyUiRedirects.PatternTemplates (InternalsVisibleTo already
set for ErsatzTV.Tests); stores the raw template on PatternRule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy
merged commit 495e450e2c into main2026-07-11 17:00:03 +02:00
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 #204 (
[PLAN-MODE], #91 phase (b) removal-plan prerequisite).Problem
LegacyUiRedirectsmatched exact paths only (aDictionarylookup). The removal plan's Step 1 — "add every SPA-READY route incl. id-carrying detail/editor routes → their/app/...equivalents" — was not expressible: routes like/channels/{id},/media/movies/{id},/playouts/{id}/templatescan't be static keys. After Blazor deletion they would hard-404.Change (two-tier matcher, signature unchanged)
Map, grown to 52 entries (the 13 foundational routes + the #204 parameterless (A)/(B) routes + the (E-base) browse roots whose targets carry?kind=…).PatternRulesegment templates ((C) id-substituting, (C2) mid-path id, (D) id-dropping detail→list, (E-page)/…/page/{n}→?kind=). First-match-wins; consulted only on a Tier-1 miss.{id}= strict positive integer (NumberStyles.None,> 0); non-int/0/negative/overflow falls through to Blazor (never a broken/app/edit-channel/abc). This strictness is also what makes the rule set collision-free by construction.AppendQueryString(target, QueryString)helper&-merges the incoming query into?kind=targets, fixing the malformed double-?(/media/movies?page=2→/app/media?kind=movies&page=2); plain targets keep verbatim-append behavior byte-for-byte. One-lineStartup.cschange; redirect stays GET/HEAD-only 302 beforeUseRouting().Guard invariant (comment +
Map-keys meta-test): no rule may begin with/api,/artwork,/docs,/openapi,/iptv,/app,/media/sources— rules are always full templates, never prefix wildcards.Scope
Completes phase-(a) Step 1 for every PARITY-OK route (
docs/blazor-route-parity.md§2 → §1). Deliberately out of scope: the catch-all fallback replacingMapFallbackToPage("/_Host")(coupled to actual Blazor deletion — the removal PR),/media/sources/*(#202, other session), and/system/health(kept escape hatch) — both encoded as negative tests.Verification
dotnet build ErsatzTV.sln— 0 errors.dotnet test ErsatzTV.Tests— 1132 passed, 0 failed (adds positive/negative/id-validation/query-merge/guard-meta cases).[PLAN-MODE]pass (mechanism, query-collision, id-validation, ordering); recorded indocs/decisions.md.Docs updated same-PR:
docs/blazor-route-parity.md(§2 rows → §1),docs/decisions.md(append).🤖 Generated with Claude Code
Extend LegacyUiRedirects from an exact-match dictionary to a two-tier matcher: Tier 1 keeps the exact Map (now 52 entries incl. the ?kind= browse roots), Tier 2 adds 36 ordered segment-template PatternRules for id-carrying routes. {id} is a strict positive integer (non-int/0/neg/overflow falls through), which also makes the rule set collision-free by construction. New AppendQueryString helper merges the incoming query into ?kind= targets with '&' (kills the double-'?' bug); one-line Startup change keeps the redirect GET/HEAD-only 302 before UseRouting. Completes phase-(a) Step 1 for every PARITY-OK route (#91 phase b); the catch-all fallback replacing MapFallbackToPage stays with the removal PR. /media/sources/* (#202) and /system/health remain deliberately un-redirected. fixes #204 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>