@page "/fleet/diagnostics" @* Per-node driver diagnostics, fetched on demand from the selected driver node's DriverHostActor over Akka (IFleetDiagnosticsClient → GetDiagnostics → NodeDiagnosticsSnapshot). The node answers from its live child set + each child's last-published health; the "running from cache" flag is the one fact /hosts cannot show (a node booted from its artifact cache looks healthy but is frozen). Reach it as /fleet/diagnostics?node= (the NodeId shown on /fleet and /hosts), or pick a driver-role member from the selector. *@ @attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)] @rendermode RenderMode.InteractiveServer @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.Cluster @using ZB.MOM.WW.OtOpcUa.Commons.Interfaces @using ZB.MOM.WW.OtOpcUa.Commons.Types @using ZB.MOM.WW.OtOpcUa.Configuration @inject IClusterRoleInfo Cluster @inject IFleetDiagnosticsClient Diagnostics @inject IDbContextFactory DbFactory @inject NavigationManager Nav @implements IDisposable

Node diagnostics

Back to fleet status
Last fetched: @(_fetchedUtc?.ToString("HH:mm:ss 'UTC'") ?? "—")
@if (string.IsNullOrEmpty(_selectedNode)) {
Pick a driver-role node to fetch its driver diagnostics. The node answers from its own DriverHostActor, so this works for any node reachable over Akka remoting from this admin node.
} else if (_snapshot is null) {

@(_loading ? "Fetching…" : "No snapshot yet.")

} else {
Node
@_snapshot.NodeId.Value
Revision
@(_snapshot.CurrentRevision is { } rev ? DisplayText.Abbreviate(rev.Value, 12) : "—")
Config source
@(_snapshot.RunningFromCache ? "cache (frozen)" : "live")
Drivers
@_snapshot.Drivers.Count
Faulted devices
@_snapshot.Drivers.Sum(d => d.FaultedDevices)
@if (_snapshot.RunningFromCache) {
This node booted its configuration from its local artifact cache because central was unreachable. It serves its last-known-good address space, but no deployment can reach it until central is back.
}
Drivers as of @_snapshot.AsOfUtc.ToString("u")
@if (_snapshot.Drivers.Count == 0) {
Empty snapshot: the node has no driver children — or it did not answer within the client's 3 s budget (an unreachable node degrades to an empty snapshot; see the AdminUI log for the FleetDiagnosticsClient warning).
} else {
@foreach (var d in _snapshot.Drivers.OrderBy(d => DisplayNameOf(d), StringComparer.OrdinalIgnoreCase)) { }
Driver instance Row id State Connected devices Faulted devices Last health publish
@* A-F5: the wire carries the string instance id; show the authored name (joined from the ConfigDb) with the id beneath it. *@ @DisplayNameOf(d) @if (!string.Equals(DisplayNameOf(d), d.Name, StringComparison.Ordinal)) {
@d.Name
}
@(d.DriverInstanceId == Guid.Empty ? "—" : DisplayText.Abbreviate(d.DriverInstanceId.ToString(), 13)) @d.State @d.ConnectedDevices @d.FaultedDevices @d.LastChangeUtc.ToString("u")
}
}
@code { [SupplyParameterFromQuery(Name = "node")] public string? Node { get; set; } private IReadOnlyList _members = Array.Empty(); private string _selectedNode = ""; private string _typedNode = ""; private NodeDiagnosticsSnapshot? _snapshot; private IReadOnlyDictionary _displayNames = new Dictionary(); private bool _loading; private DateTime? _fetchedUtc; // Page-lifetime cancellation: a fetch in flight when the circuit goes away is abandoned. private readonly CancellationTokenSource _cts = new(); private volatile bool _disposed; protected override async Task OnParametersSetAsync() { _members = Cluster.MembersWithRole(RoleParser.Driver) .Select(n => n.Value) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) .ToList(); var requested = Node?.Trim() ?? ""; if (!string.Equals(requested, _selectedNode, StringComparison.Ordinal)) { _selectedNode = requested; _snapshot = null; if (!string.IsNullOrEmpty(_selectedNode)) await LoadAsync(); } } private Task OnNodeSelected(ChangeEventArgs e) { var value = e.Value?.ToString() ?? ""; Navigate(value); return Task.CompletedTask; } private Task UseTypedNodeAsync() { Navigate(_typedNode.Trim()); return Task.CompletedTask; } private Task OnTypedKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter") Navigate(_typedNode.Trim()); return Task.CompletedTask; } // The node id lives in the URL so a diagnostics view is linkable from /fleet and survives a reload. private void Navigate(string node) => Nav.NavigateTo(string.IsNullOrEmpty(node) ? "/fleet/diagnostics" : FleetDiagnosticsLinks.For(node)); private async Task RefreshAsync() => await LoadAsync(); private async Task LoadAsync() { if (_loading || string.IsNullOrEmpty(_selectedNode)) return; _loading = true; try { // The client is fail-safe by contract: a node that does not answer yields an empty snapshot. _snapshot = await Diagnostics.GetDiagnosticsAsync(NodeId.Parse(_selectedNode), _cts.Token); _fetchedUtc = DateTime.UtcNow; _displayNames = await LoadDisplayNamesAsync(_snapshot); } catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) { // Circuit gone mid-fetch — a clean exit, not a fault. } finally { _loading = false; if (!_disposed) StateHasChanged(); } } // The authored name for a driver row, else its id (no ConfigDb row, or the name lookup failed). private string DisplayNameOf(DriverInstanceDiagnostics d) => _displayNames.TryGetValue(d.Name, out var name) ? name : d.Name; private async Task> LoadDisplayNamesAsync(NodeDiagnosticsSnapshot snapshot) { try { await using var db = await DbFactory.CreateDbContextAsync(_cts.Token); return await FleetDiagnosticsLinks.LoadDisplayNamesAsync( db, snapshot.Drivers.Select(d => d.Name).ToList(), _cts.Token); } catch (Exception ex) when (ex is not OperationCanceledException and not ObjectDisposedException) { // Names are enrichment only: a ConfigDb hiccup falls back to the ids, never fails the page. return new Dictionary(); } } private static string StateChipClass(string state) => state switch { "Healthy" => "chip-ok", "Degraded" or "Connecting" or "Reconnecting" => "chip-caution", "Faulted" => "chip-alert", _ => "chip-idle", }; public void Dispose() { _disposed = true; _cts.Cancel(); _cts.Dispose(); } }