From d44fe1d6b5a0e17abab957a4c122c18e2811a4f4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 15 Aug 2026 20:10:51 -0400 Subject: [PATCH] feat(dashboard): AlarmsPage reads provider status from IGatewayAlarmService in-process --- .../Components/Pages/AlarmsPage.razor | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor index 93180af..2aecd24 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor @@ -1,10 +1,9 @@ @page "/alarms" @implements IAsyncDisposable -@using Microsoft.AspNetCore.SignalR.Client -@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs +@using ZB.MOM.WW.MxGateway.Server.Alarms @inject IDashboardLiveDataService LiveData @inject IOptions GatewayOptions -@inject DashboardHubConnectionFactory HubFactory +@inject IGatewayAlarmService AlarmService Dashboard Alarms @@ -173,13 +172,13 @@ private Task? _pollTask; private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy; - private HubConnection? _alarmsHub; + private Task? _providerStatusTask; /// protected override void OnInitialized() { _pollTask = PollLoopAsync(); - _ = AttachAlarmsHubAsync(); + _providerStatusTask = ProviderStatusLoopAsync(); } private string? ProviderStatusTitle() @@ -189,26 +188,48 @@ : null; } - private async Task AttachAlarmsHubAsync() + // The badge tracks the central monitor directly rather than looping back through + // /hubs/alarms: the alarm service is an in-process multi-subscriber fan-out, so a + // server-rendered page needs no SignalR client, no loopback socket and no auth token. + // Alarm rows still come from the 3-second poll below — this loop only feeds the badge. + private async Task ProviderStatusLoopAsync() { - _alarmsHub = HubFactory.Create("/hubs/alarms"); - _alarmsHub.On(AlarmsHub.AlarmMessage, async message => + while (!_cts.IsCancellationRequested) { - if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus) + try { - _providerStatus = DashboardAlarmProviderStatus.FromFeed(message); - await InvokeAsync(StateHasChanged).ConfigureAwait(false); - } - }); + await foreach (AlarmFeedMessage message in AlarmService + .StreamAsync(alarmFilterPrefix: null, _cts.Token) + .ConfigureAwait(false)) + { + if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus) + { + continue; + } - try - { - await _alarmsHub.StartAsync(_cts.Token).ConfigureAwait(false); - } - catch - { - // The badge is best-effort; it stays at the healthy default until - // the hub reconnects and delivers a fresh provider-status message. + _providerStatus = DashboardAlarmProviderStatus.FromFeed(message); + await InvokeAsync(StateHasChanged).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + return; + } + catch + { + // The monitor completes a subscriber's stream when it falls behind, and + // again when the monitor restarts. Both are recoverable by resubscribing; + // the badge holds its last value in the meantime. + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } } } @@ -312,18 +333,6 @@ { await _cts.CancelAsync(); - if (_alarmsHub is not null) - { - try - { - await _alarmsHub.DisposeAsync(); - } - catch - { - // Disposal-time errors are best-effort. - } - } - if (_pollTask is not null) { try @@ -335,6 +344,17 @@ } } + if (_providerStatusTask is not null) + { + try + { + await _providerStatusTask; + } + catch (OperationCanceledException) + { + } + } + _cts.Dispose(); GC.SuppressFinalize(this); }