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>
75 lines
2.9 KiB
C#
75 lines
2.9 KiB
C#
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;
|
|
}
|
|
}
|