refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,121 @@
@implements IDisposable
@inject NavigationManager Navigation
@inject IJSRuntime JS
@code {
// CentralUI-005 / CentralUI-020: session expiry is a sliding window owned by
// the cookie authentication middleware (ZB.MOM.WW.ScadaBridge.Security AddCookie:
// ExpireTimeSpan = idle timeout, SlidingExpiration = true). An active user's
// cookie is continually renewed; an idle user's cookie lapses after the idle
// timeout. There is no fixed login-time deadline to redirect at.
//
// This component must NOT poll the Blazor AuthenticationStateProvider:
// CookieAuthenticationStateProvider serves a frozen constructor-time
// principal for the whole circuit (CentralUI-004), so the polled auth state
// can never transition to "expired" and the redirect would never fire
// (CentralUI-020).
//
// Instead it polls the server endpoint GET /auth/ping via fetch(). Being a
// normal HTTP request, the cookie middleware re-validates — and slides — the
// cookie on every hit, and answers 200 while the session is live or 401 once
// it has lapsed. A genuine idle user's circuit produces no other HTTP
// traffic, so once the cookie lapses the next ping returns 401 and this
// component redirects to /login. (The ping itself slides the cookie, but the
// poll interval is well under the idle timeout, so an idle session still
// lapses on schedule once the poll catches the lapsed state — the ping only
// ever observes expiry, it does not keep a dead session alive.)
/// <summary>Server endpoint that reports live session validity.</summary>
internal const string PingUrl = "/auth/ping";
/// <summary>HTTP status returned by <see cref="PingUrl"/> once the cookie has lapsed.</summary>
private const int Unauthorized = 401;
private const string ModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/session-expiry.js";
/// <summary>How often the session validity is re-checked.</summary>
internal static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(1);
private CancellationTokenSource? _cts;
private IJSObjectReference? _module;
protected override void OnInitialized()
{
// The login page uses the same layout, so this component renders there
// too. Polling/redirecting on /login → /login would loop.
if (IsOnLoginPage) return;
_cts = new CancellationTokenSource();
_ = PollSessionAsync(_cts.Token);
}
private bool IsOnLoginPage =>
Navigation.ToBaseRelativePath(Navigation.Uri)
.StartsWith("login", StringComparison.OrdinalIgnoreCase);
private async Task PollSessionAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try { await Task.Delay(PollInterval, token); }
catch (TaskCanceledException) { return; }
if (token.IsCancellationRequested) return;
await CheckSessionAsync();
}
}
/// <summary>
/// Runs one liveness check: pings the server and, if the session has lapsed
/// server-side (HTTP 401), redirects to the login page. Exposed for tests
/// (CentralUI-025) so the redirect path can be exercised without waiting on
/// the poll interval.
/// </summary>
internal async Task CheckSessionAsync()
{
if (IsOnLoginPage) return;
int status;
try
{
_module ??= await JS.InvokeAsync<IJSObjectReference>("import", ModulePath);
status = await _module.InvokeAsync<int>("ping", PingUrl);
}
catch (JSDisconnectedException)
{
// Circuit gone — nothing to redirect.
return;
}
catch (JSException)
{
// Network blip or fetch failure: treat as inconclusive and retry on
// the next poll rather than logging an authenticated user out on a
// transient error.
return;
}
if (status == Unauthorized)
{
await InvokeAsync(() => Navigation.NavigateTo("/login", forceLoad: true));
}
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
// The module reference is owned by the circuit's JS runtime; once the
// circuit is disposed disposing it would throw — fire-and-forget and
// swallow the expected disconnect.
if (_module is not null)
{
_ = DisposeModuleAsync(_module);
}
}
private static async Task DisposeModuleAsync(IJSObjectReference module)
{
try { await module.DisposeAsync(); }
catch (JSDisconnectedException) { /* circuit already gone */ }
}
}