feat(web): SPA cutover — root route + legacy redirects (#91)
Make the ChicoryTV SPA the default UI: GET/HEAD requests to / and to legacy Blazor routes with SPA equivalents (channels, schedules, playouts, libraries, all settings pages, channel add) now 302 to their /app counterparts, preserving query strings and the ETV_BASE_URL path base. 302 not 301: the map will grow as parity lands and permanent- redirect caching would make rollback painful. Blazor-only functionality (collections, media browse/search, trakt, filler presets, watermarks, ffmpeg profiles, blocks/decos/templates, playout detail editors, logs, troubleshooting, channel edit) keeps serving Blazor; the Blazor home stays reachable at /system/health. Parity gaps are tracked in #140-#146; Blazor removal is phase (b). Also adds an /app smoke assertion to the docker build workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace ErsatzTV;
|
||||
|
||||
// Phase (a) of the Blazor -> ChicoryTV SPA cutover (ersatztv#91).
|
||||
//
|
||||
// The React SPA (served under /app) is now the default UI: the legacy Blazor
|
||||
// routes below 302-redirect to their SPA equivalents. Only routes that already
|
||||
// have SPA parity are listed here. Blazor pages WITHOUT a SPA equivalent are
|
||||
// deliberately left reachable (no redirect) so their functionality stays
|
||||
// available while the SPA catches up:
|
||||
// /system/health (Blazor home escape hatch; Index.razor also lives here),
|
||||
// /channels/{id} edit, /channels/numbers, /media/* (collections etc.),
|
||||
// /ffmpeg, /watermarks, /blocks, /decos, /templates, /deco-templates,
|
||||
// schedule/playout detail editors, /system/logs, /system/troubleshooting.
|
||||
//
|
||||
// Phase (b) removes the redirected Blazor pages entirely, but that is GATED on
|
||||
// full SPA parity for every route in this map. Until then this map is the
|
||||
// single source of truth for "what has migrated" and is expected to grow.
|
||||
public static class LegacyUiRedirects
|
||||
{
|
||||
// Blazor route -> SPA route. EXACT paths only (no prefix matching); a single
|
||||
// trailing slash on the request is normalized away before lookup.
|
||||
public static readonly IReadOnlyDictionary<string, string> Map =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["/"] = "/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"
|
||||
};
|
||||
|
||||
public static bool TryGetRedirect(PathString path, out string target)
|
||||
{
|
||||
target = string.Empty;
|
||||
|
||||
string value = path.Value;
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize a single trailing slash so "/channels/" matches "/channels"
|
||||
// (but keep root "/" intact). Guard against a path of all slashes (e.g.
|
||||
// "//") collapsing down to "/" and falsely matching the root entry.
|
||||
if (value.Length > 1 && value.EndsWith('/'))
|
||||
{
|
||||
string trimmed = value[..^1];
|
||||
if (trimmed == "/")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = trimmed;
|
||||
}
|
||||
|
||||
if (Map.TryGetValue(value, out string mapped))
|
||||
{
|
||||
target = mapped;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -726,6 +726,28 @@ public class Startup
|
||||
ctx => !IsIptvPath(ctx.Request.Path) && !IsSpaPath(ctx.Request.Path),
|
||||
blazor =>
|
||||
{
|
||||
// ersatztv#91 phase (a): make the ChicoryTV SPA the default UI by
|
||||
// redirecting migrated legacy Blazor routes to their /app equivalents.
|
||||
// 302 (not 301): this map grows as pages migrate, and permanent-redirect
|
||||
// browser caching would make rollback painful. UsePathBase (ETV_BASE_URL)
|
||||
// only rewrites the request side (Request.Path/PathBase); it never touches
|
||||
// redirect Location headers, so the PathBase prefix must be re-applied here.
|
||||
blazor.Use(async (context, next) =>
|
||||
{
|
||||
if (HttpMethods.IsGet(context.Request.Method) ||
|
||||
HttpMethods.IsHead(context.Request.Method))
|
||||
{
|
||||
if (LegacyUiRedirects.TryGetRedirect(context.Request.Path, out string target))
|
||||
{
|
||||
context.Response.Redirect(
|
||||
context.Request.PathBase + target + context.Request.QueryString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
blazor.UseRouting();
|
||||
|
||||
if (OidcHelper.IsEnabled)
|
||||
|
||||
Reference in New Issue
Block a user