@page "/hosts" @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.Admin.Services @using ZB.MOM.WW.OtOpcUa.Configuration.Enums @inject IServiceScopeFactory ScopeFactory @implements IDisposable

Driver host status

Auto-refresh every @RefreshIntervalSeconds s. Last updated: @(_lastRefreshUtc?.ToString("HH:mm:ss 'UTC'") ?? "—")
Each row is one host reported by a driver instance on a server node. Galaxy drivers report per-Platform / per-AppEngine entries; Modbus drivers report the PLC endpoint. Rows age out of the Server's publisher on every 10-second heartbeat — rows whose LastSeen is older than 30s are flagged Stale, which usually means the owning Server process has crashed or lost its DB connection.
@if (_rows is null) {

Loading…

} else if (_rows.Count == 0) {
No host-status rows yet. The Server publishes its first tick 2s after startup; if this list stays empty, check that the Server is running and the driver implements IHostConnectivityProbe.
} else {
Hosts
@_rows.Count
Running
@_rows.Count(r => r.State == DriverHostState.Running && !HostStatusService.IsStale(r))
Stale
@_rows.Count(HostStatusService.IsStale)
Faulted
@_rows.Count(r => r.State == DriverHostState.Faulted)
@foreach (var cluster in _rows.GroupBy(r => r.ClusterId ?? "(unassigned)").OrderBy(g => g.Key)) {

Cluster: @cluster.Key

@foreach (var r in cluster) { }
Node Driver Host State Last transition Last seen Detail
@r.NodeId @r.DriverInstanceId @r.HostName @r.State @if (HostStatusService.IsStale(r)) { Stale } @FormatAge(r.StateChangedUtc) @FormatAge(r.LastSeenUtc) @r.Detail
} } @code { // Mirrors HostStatusPublisher.HeartbeatInterval — polling ahead of the broadcaster // produces stale-looking rows mid-cycle. private const int RefreshIntervalSeconds = 10; private List? _rows; private bool _refreshing; private DateTime? _lastRefreshUtc; private Timer? _timer; protected override async Task OnInitializedAsync() { await RefreshAsync(); _timer = new Timer(async _ => await InvokeAsync(RefreshAsync), state: null, dueTime: TimeSpan.FromSeconds(RefreshIntervalSeconds), period: TimeSpan.FromSeconds(RefreshIntervalSeconds)); } private async Task RefreshAsync() { if (_refreshing) return; _refreshing = true; try { using var scope = ScopeFactory.CreateScope(); var svc = scope.ServiceProvider.GetRequiredService(); _rows = (await svc.ListAsync()).ToList(); _lastRefreshUtc = DateTime.UtcNow; } finally { _refreshing = false; StateHasChanged(); } } private static string RowClass(HostStatusRow r) => r.State switch { DriverHostState.Faulted => "table-danger", _ when HostStatusService.IsStale(r) => "table-warning", _ => "", }; private static string StateBadge(DriverHostState s) => s switch { DriverHostState.Running => "bg-success", DriverHostState.Stopped => "bg-secondary", DriverHostState.Faulted => "bg-danger", _ => "bg-secondary", }; private static string FormatAge(DateTime t) { var age = DateTime.UtcNow - t; if (age.TotalSeconds < 60) return $"{(int)age.TotalSeconds}s ago"; if (age.TotalMinutes < 60) return $"{(int)age.TotalMinutes}m ago"; if (age.TotalHours < 24) return $"{(int)age.TotalHours}h ago"; return t.ToString("yyyy-MM-dd HH:mm 'UTC'"); } public void Dispose() => _timer?.Dispose(); }