@page "/fleet" @* Per-node deployment status. v2 reads NodeDeploymentState (the per-(node, deployment) apply progress row owned by each DriverHostActor) and projects the most-recent row per node. The Akka cluster topology comes from IClusterRoleInfo so we can show nodes that haven't applied anything yet alongside nodes that have. *@ @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.Commons.Interfaces @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Configuration.Enums @inject IClusterRoleInfo Cluster @inject IDbContextFactory DbFactory @implements IDisposable

Fleet status

Auto-refresh every @RefreshIntervalSeconds s. Last updated: @(_lastRefreshUtc?.ToString("HH:mm:ss 'UTC'") ?? "—")
@if (_rows is null) {

Loading…

} else if (_rows.Count == 0) {
No driver-role nodes are currently Up in the Akka cluster, and no NodeDeploymentState rows have been recorded yet. Either no driver nodes have joined or the cluster is still forming.
} else {
Nodes
@_rows.Count
Applied
@_rows.Count(r => r.Status == NodeDeploymentStatus.Applied)
Applying
@_rows.Count(r => r.Status == NodeDeploymentStatus.Applying)
Failed
@_rows.Count(r => r.Status == NodeDeploymentStatus.Failed)
Nodes
@foreach (var r in _rows) { }
Node Roles Status Applied at Started at Failure reason
@r.NodeId @foreach (var role in r.Roles) { @role } @StatusLabel(r.Status) @(r.AppliedAtUtc?.ToString("u") ?? "—") @(r.StartedAtUtc?.ToString("u") ?? "—") @(r.FailureReason ?? "")
} @code { private const int RefreshIntervalSeconds = 10; private List? _rows; private bool _refreshing; private DateTime? _lastRefreshUtc; private Timer? _timer; protected override async Task OnInitializedAsync() { await LoadAsync(); _timer = new Timer(_ => _ = InvokeAsync(LoadAsync), null, TimeSpan.FromSeconds(RefreshIntervalSeconds), TimeSpan.FromSeconds(RefreshIntervalSeconds)); } private async Task RefreshAsync() => await LoadAsync(); private async Task LoadAsync() { _refreshing = true; StateHasChanged(); try { await using var db = await DbFactory.CreateDbContextAsync(); // Project the most-recent NodeDeploymentState per node — that's the row the // DriverHostActor most recently touched, regardless of which deployment it was for. var states = await db.NodeDeploymentStates.AsNoTracking() .GroupBy(s => s.NodeId) .Select(g => g.OrderByDescending(s => s.StartedAtUtc).First()) .ToListAsync(); var byNode = states.ToDictionary(s => s.NodeId); // Union with current Akka driver members so a freshly-joined node that has no // NodeDeploymentState row yet still appears as "waiting". var akkaDrivers = Cluster.MembersWithRole("driver") .Select(n => n.Value).ToHashSet(StringComparer.OrdinalIgnoreCase); var allNodes = byNode.Keys.Union(akkaDrivers, StringComparer.OrdinalIgnoreCase) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(); _rows = allNodes.Select(nodeId => { byNode.TryGetValue(nodeId, out var state); return new NodeRow( NodeId: nodeId, Roles: akkaDrivers.Contains(nodeId) ? new[] { "driver" } : Array.Empty(), Status: state?.Status, StartedAtUtc: state?.StartedAtUtc, AppliedAtUtc: state?.AppliedAtUtc, FailureReason: state?.FailureReason); }).ToList(); _lastRefreshUtc = DateTime.UtcNow; } finally { _refreshing = false; StateHasChanged(); } } private static string StatusChipClass(NodeDeploymentStatus? status) => status switch { NodeDeploymentStatus.Applied => "chip-ok", NodeDeploymentStatus.Applying => "chip-caution", NodeDeploymentStatus.Failed => "chip-alert", _ => "chip-idle", }; private static string StatusLabel(NodeDeploymentStatus? status) => status?.ToString() ?? "waiting"; public void Dispose() => _timer?.Dispose(); private sealed record NodeRow( string NodeId, IReadOnlyCollection Roles, NodeDeploymentStatus? Status, DateTime? StartedAtUtc, DateTime? AppliedAtUtc, string? FailureReason); }