From 4584612a1a580c6c23b1f45c8a5d85d504f57b57 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 28 May 2026 10:29:43 -0400 Subject: [PATCH] feat(adminui): DriverStatusPanel + wire into 9 typed pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live panel subscribed to the /hubs/driverstatus SignalR feed — renders state chip, last-success age, 5-min error count, last error message. Auto-reconnect; dimmed when no push arrives for 30s. Hidden for new instances (nothing deployed yet); shown read-only on every edit-mode page. Reconnect/Restart buttons land in Phase 8. --- .../Clusters/Drivers/AbCipDriverPage.razor | 5 + .../Clusters/Drivers/AbLegacyDriverPage.razor | 5 + .../Clusters/Drivers/FocasDriverPage.razor | 5 + .../Clusters/Drivers/GalaxyDriverPage.razor | 5 + .../HistorianWonderwareDriverPage.razor | 5 + .../Clusters/Drivers/ModbusDriverPage.razor | 5 + .../Drivers/OpcUaClientDriverPage.razor | 5 + .../Pages/Clusters/Drivers/S7DriverPage.razor | 5 + .../Clusters/Drivers/TwinCATDriverPage.razor | 5 + .../Shared/Drivers/DriverStatusPanel.razor | 152 ++++++++++++++++++ 10 files changed, 197 insertions(+) create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor index 6198b0a1..4f92011b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Operation timeout *@
Operation settings
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor index 93b0128c..3763380b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor @@ -37,6 +37,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Operation settings *@
Operation settings
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor index c22183af..d6a5a0af 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Connection *@
Connection
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor index d47a672e..2014072f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* mxaccessgw connection *@
mxaccessgw connection
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor index 6e0bc73d..12b3cc13 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Connection *@
Connection
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor index 031a28e1..48541af7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Transport *@
Transport
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor index 83231acf..8479d06c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Endpoint *@
Endpoint
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor index df208cf3..67304ea4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Connection *@
Connection
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor index 0c45af66..1f5db0da 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor @@ -36,6 +36,11 @@ else + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) + { + + } + @* Options *@
Options
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor new file mode 100644 index 00000000..8ab11c43 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor @@ -0,0 +1,152 @@ +@* Live driver-status panel — subscribes to /hubs/driverstatus and shows state chip, + last-success age, 5-min error count, and last error message. + Enabled=false renders a static "Disabled" notice and never opens the hub. + Reconnect/Restart buttons are Phase 8 (Task 8.3). *@ +@implements IAsyncDisposable +@using Microsoft.AspNetCore.SignalR.Client +@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers +@inject NavigationManager Nav + +
+
+ Driver status + @if (_snapshot is not null) + { + @_snapshot.State + } + else if (!Enabled) + { + Disabled + } + else if (_connecting) + { + Connecting… + } +
+ +
+ @if (!Enabled) + { +

Disabled — not deployed. Enable the driver and save to start receiving live status.

+ } + else if (_error is not null) + { +

SignalR error: @_error

+ } + else if (_snapshot is null) + { +

Awaiting first snapshot…

+ } + else + { +
+ + Last success: + @if (_snapshot.LastSuccessfulReadUtc is { } t) + { + @HumanizeAge(t) ago + } + else + { + never + } + + + @if (_snapshot.ErrorCount5Min > 0) + { + @_snapshot.ErrorCount5Min error@(_snapshot.ErrorCount5Min == 1 ? "" : "s") / 5 min + } +
+ + @if (_snapshot.LastError is { Length: > 0 } lastError) + { +
+ Last error +
@lastError
+
+ } + } +
+
+ +@code { + [Parameter, EditorRequired] public string DriverInstanceId { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + private HubConnection? _hub; + private DriverHealthChanged? _snapshot; + private DateTime _lastUpdateUtc = DateTime.MinValue; + private bool _stale; + private bool _connecting; + private string? _error; + private System.Threading.Timer? _timer; + + protected override async Task OnInitializedAsync() + { + if (!Enabled) + return; + + _connecting = true; + + // Tick every 5 s to refresh the stale-dimming check and humanized ages. + _timer = new System.Threading.Timer(_ => + { + _stale = _snapshot is not null && + (DateTime.UtcNow - _lastUpdateUtc).TotalSeconds > 30; + InvokeAsync(StateHasChanged); + }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + + _hub = new HubConnectionBuilder() + .WithUrl(Nav.ToAbsoluteUri("/hubs/driverstatus")) + .WithAutomaticReconnect() + .Build(); + + _hub.On("status", snap => + { + _snapshot = snap; + _lastUpdateUtc = DateTime.UtcNow; + _stale = false; + InvokeAsync(StateHasChanged); + }); + + try + { + await _hub.StartAsync(); + _connecting = false; + await _hub.InvokeAsync("JoinDriver", DriverInstanceId); + } + catch (Exception ex) + { + _connecting = false; + _error = ex.Message; + } + } + + public async ValueTask DisposeAsync() + { + _timer?.Dispose(); + if (_hub is not null) + await _hub.DisposeAsync(); + } + + // Map DriverState string → chip CSS class using the 4 defined theme variants. + private static string ChipClass(string? state) => state switch + { + "Healthy" => "chip-ok", + "Degraded" => "chip-warn", + "Connecting" => "chip-warn", + "Reconnecting" => "chip-warn", + "Faulted" => "chip-bad", + _ => "chip-idle", // Unknown, Initializing, null + }; + + private static string HumanizeAge(DateTime utc) + { + var age = DateTime.UtcNow - utc; + if (age.TotalSeconds < 2) return "just now"; + if (age.TotalSeconds < 60) return $"{(int)age.TotalSeconds}s"; + if (age.TotalMinutes < 60) return $"{(int)age.TotalMinutes}m {age.Seconds}s"; + if (age.TotalHours < 24) return $"{(int)age.TotalHours}h {age.Minutes}m"; + return $"{(int)age.TotalDays}d {age.Hours}h"; + } +}