chore(cleanup): delete OtOpcUa.Server, OtOpcUa.Admin, and obsolete v1 tests

Task 56: removes the legacy in-process Server + Admin Web project + their test
projects (Server.Tests, Admin.Tests, Admin.E2ETests). The fused OtOpcUa.Host
binary built across Phases 1-9 is now the sole production entry point.

What happened to the 47 legacy Admin Blazor pages: per follow-up F15, the
v1 architecture's draft/publish UX is replaced by v2's live-edit + snapshot-
deploy model, so a 1:1 migration is not meaningful. The mechanical move via
git mv preserves the history; service classes + page bodies that referenced
removed v1 types (ConfigGeneration, RedundancyRole, GenerationId) were
deleted. AdminUI now ships a minimal Home page + the v2 Deployments page.

Per-page rebuild against the v2 surface is tracked as F15. The v2 Deployments
page (Task 52) is the only first-party UI shipping in this PR.

Task 57: solution build green; 84+ tests green across active v2 + legacy
driver test projects.
This commit is contained in:
Joseph Doherty
2026-05-26 05:38:31 -04:00
parent 2b75ce3876
commit 76310b8829
258 changed files with 29 additions and 33514 deletions
@@ -0,0 +1,77 @@
@inherits LayoutComponentBase
<header class="app-bar">
<span class="brand"><span class="mark">&#9646;</span> OtOpcUa</span>
<span class="crumb">&rsaquo;</span>
<span class="crumb">admin console</span>
<span class="spacer"></span>
<AuthorizeView>
<Authorized>
<span class="meta">@context.User.Identity?.Name</span>
<span class="conn-pill" data-state="connected">
<span class="dot"></span><span>signed in</span>
</span>
</Authorized>
<NotAuthorized>
<span class="conn-pill" data-state="disconnected">
<span class="dot"></span><span>signed out</span>
</span>
</NotAuthorized>
</AuthorizeView>
</header>
<div class="app-shell d-flex flex-column flex-lg-row">
@* Hamburger toggle: visible only on viewports <lg.
Bootstrap collapse JS lives in bootstrap.bundle.min.js (loaded in App.razor). *@
<button class="btn btn-outline-secondary btn-sm d-lg-none m-2 align-self-start"
type="button"
data-bs-toggle="collapse"
data-bs-target="#sidebar-collapse"
aria-controls="sidebar-collapse"
aria-expanded="false"
aria-label="Toggle navigation">
&#9776;
</button>
<div class="collapse d-lg-block" id="sidebar-collapse">
<nav class="side-rail">
<div class="rail-eyebrow">Navigation</div>
<NavLink class="rail-link" href="/" Match="NavLinkMatch.All">Overview</NavLink>
<NavLink class="rail-link" href="/fleet" Match="NavLinkMatch.Prefix">Fleet status</NavLink>
<NavLink class="rail-link" href="/hosts" Match="NavLinkMatch.Prefix">Host status</NavLink>
<NavLink class="rail-link" href="/clusters" Match="NavLinkMatch.Prefix">Clusters</NavLink>
<NavLink class="rail-link" href="/reservations" Match="NavLinkMatch.Prefix">Reservations</NavLink>
<NavLink class="rail-link" href="/certificates" Match="NavLinkMatch.Prefix">Certificates</NavLink>
<NavLink class="rail-link" href="/role-grants" Match="NavLinkMatch.Prefix">Role grants</NavLink>
<div class="rail-eyebrow">Scripting</div>
<NavLink class="rail-link" href="/virtual-tags" Match="NavLinkMatch.Prefix">Virtual tags</NavLink>
<NavLink class="rail-link" href="/scripted-alarms" Match="NavLinkMatch.Prefix">Scripted alarms</NavLink>
<NavLink class="rail-link" href="/script-log" Match="NavLinkMatch.Prefix">Script log</NavLink>
<div class="rail-foot">
<AuthorizeView>
<Authorized>
<div class="rail-eyebrow">Session</div>
<a class="rail-user" href="/account">@context.User.Identity?.Name</a>
<div class="rail-roles">
@string.Join(", ", context.User.Claims
.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value))
</div>
<form method="post" action="/auth/logout">
<AntiforgeryToken />
<button class="rail-btn" type="submit">Sign out</button>
</form>
</Authorized>
<NotAuthorized>
<div class="rail-eyebrow">Session</div>
<a class="rail-btn" href="/login">Sign in</a>
</NotAuthorized>
</AuthorizeView>
</div>
</nav>
</div>
<main class="page">
@Body
</main>
</div>
@@ -0,0 +1,7 @@
@page "/"
<PageTitle>OtOpcUa</PageTitle>
<h1>OtOpcUa Admin</h1>
<p>v2 fused host. Use the nav above to manage deployments.</p>
<p class="text-muted">Most v1 admin pages were removed by the live-edit migration — see follow-up F15 for the per-page restoration plan.</p>
@@ -0,0 +1,17 @@
@* Reusable loading spinner *@
@if (IsLoading)
{
<div class="d-flex align-items-center text-secondary @CssClass">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<span>@Message</span>
</div>
}
@code {
[Parameter] public bool IsLoading { get; set; }
[Parameter] public string Message { get; set; } = "Loading...";
[Parameter] public string CssClass { get; set; } = "";
}
@@ -0,0 +1,7 @@
@* Status chip — wraps the theme.css .chip / .chip-ok / .chip-warn / .chip-bad / .chip-idle classes. *@
<span class="chip @CssClass">@Text</span>
@code {
[Parameter] public string Text { get; set; } = "";
[Parameter] public string CssClass { get; set; } = "chip-idle";
}
@@ -0,0 +1,139 @@
@*
Reusable toast notification component.
Toasts intentionally float above modal dialogs so confirmation feedback
(Success/Error) is visible even while a dialog is open.
*@
@implements IDisposable
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1090;" aria-live="polite" aria-atomic="true">
@foreach (var toast in _toasts)
{
<div class="toast show mb-2" role="alert">
<div class="toast-header @GetHeaderClass(toast.Type)">
<strong class="me-auto">@toast.Title</strong>
<button type="button" class="btn-close btn-close-white" @onclick="() => Dismiss(toast)"></button>
</div>
<div class="toast-body">@toast.Message</div>
</div>
}
</div>
@code {
private const int DefaultAutoDismissMs = 5000;
private readonly List<ToastItem> _toasts = new();
private readonly object _lock = new();
// Cancels all pending auto-dismiss delays when the component is disposed
// so their continuations never touch a disposed component.
private readonly CancellationTokenSource _disposalCts = new();
/// <summary>Number of toasts currently displayed.</summary>
public int ToastCount
{
get { lock (_lock) { return _toasts.Count; } }
}
public void ShowSuccess(string message, string title = "Success", int? autoDismissMs = null)
{
AddToast(title, message, ToastType.Success, autoDismissMs);
}
public void ShowError(string message, string title = "Error", int? autoDismissMs = null)
{
AddToast(title, message, ToastType.Error, autoDismissMs);
}
public void ShowWarning(string message, string title = "Warning", int? autoDismissMs = null)
{
AddToast(title, message, ToastType.Warning, autoDismissMs);
}
public void ShowInfo(string message, string title = "Info", int? autoDismissMs = null)
{
AddToast(title, message, ToastType.Info, autoDismissMs);
}
private void AddToast(string title, string message, ToastType type, int? autoDismissMs)
{
// If the component is already disposed, do not add or schedule anything.
if (_disposalCts.IsCancellationRequested) return;
var toast = new ToastItem { Title = title, Message = message, Type = type };
lock (_lock)
{
_toasts.Add(toast);
}
StateHasChanged();
var dismissMs = autoDismissMs ?? DefaultAutoDismissMs;
_ = AutoDismissAsync(toast, dismissMs, _disposalCts.Token);
}
/// <summary>
/// Removes a toast after its dismiss delay. The delay is bound to the
/// component's disposal token: if the host page is disposed first, the
/// delay is cancelled and the continuation never touches the disposed
/// component — no <see cref="ObjectDisposedException"/> escapes.
/// </summary>
private async Task AutoDismissAsync(ToastItem toast, int dismissMs, CancellationToken token)
{
try
{
await Task.Delay(dismissMs, token);
}
catch (OperationCanceledException)
{
return;
}
if (token.IsCancellationRequested) return;
lock (_lock)
{
_toasts.Remove(toast);
}
try
{
await InvokeAsync(StateHasChanged);
}
catch (ObjectDisposedException)
{
// Component disposed between the token check and the render — ignore.
}
}
private void Dismiss(ToastItem toast)
{
lock (_lock)
{
_toasts.Remove(toast);
}
}
private static string GetHeaderClass(ToastType type) => type switch
{
ToastType.Success => "bg-success text-white",
ToastType.Error => "bg-danger text-white",
ToastType.Warning => "bg-warning text-dark",
ToastType.Info => "bg-info text-dark",
_ => "bg-secondary text-white"
};
public void Dispose()
{
_disposalCts.Cancel();
_disposalCts.Dispose();
}
private enum ToastType { Success, Error, Warning, Info }
private class ToastItem
{
public string Title { get; init; } = "";
public string Message { get; init; } = "";
public ToastType Type { get; init; }
}
}