feat(dashboard): AlarmsPage reads provider status from IGatewayAlarmService in-process

This commit is contained in:
Joseph Doherty
2026-08-15 20:10:51 -04:00
parent 59420d8568
commit d44fe1d6b5
@@ -1,10 +1,9 @@
@page "/alarms" @page "/alarms"
@implements IAsyncDisposable @implements IAsyncDisposable
@using Microsoft.AspNetCore.SignalR.Client @using ZB.MOM.WW.MxGateway.Server.Alarms
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
@inject IDashboardLiveDataService LiveData @inject IDashboardLiveDataService LiveData
@inject IOptions<GatewayOptions> GatewayOptions @inject IOptions<GatewayOptions> GatewayOptions
@inject DashboardHubConnectionFactory HubFactory @inject IGatewayAlarmService AlarmService
<PageTitle>Dashboard Alarms</PageTitle> <PageTitle>Dashboard Alarms</PageTitle>
@@ -173,13 +172,13 @@
private Task? _pollTask; private Task? _pollTask;
private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy; private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
private HubConnection? _alarmsHub; private Task? _providerStatusTask;
/// <inheritdoc /> /// <inheritdoc />
protected override void OnInitialized() protected override void OnInitialized()
{ {
_pollTask = PollLoopAsync(); _pollTask = PollLoopAsync();
_ = AttachAlarmsHubAsync(); _providerStatusTask = ProviderStatusLoopAsync();
} }
private string? ProviderStatusTitle() private string? ProviderStatusTitle()
@@ -189,26 +188,48 @@
: null; : 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"); while (!_cts.IsCancellationRequested)
_alarmsHub.On<AlarmFeedMessage>(AlarmsHub.AlarmMessage, async message =>
{ {
if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus) try
{ {
_providerStatus = DashboardAlarmProviderStatus.FromFeed(message); await foreach (AlarmFeedMessage message in AlarmService
await InvokeAsync(StateHasChanged).ConfigureAwait(false); .StreamAsync(alarmFilterPrefix: null, _cts.Token)
} .ConfigureAwait(false))
}); {
if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
{
continue;
}
try _providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
{ await InvokeAsync(StateHasChanged).ConfigureAwait(false);
await _alarmsHub.StartAsync(_cts.Token).ConfigureAwait(false); }
} }
catch catch (OperationCanceledException)
{ {
// The badge is best-effort; it stays at the healthy default until return;
// the hub reconnects and delivers a fresh provider-status message. }
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(); await _cts.CancelAsync();
if (_alarmsHub is not null)
{
try
{
await _alarmsHub.DisposeAsync();
}
catch
{
// Disposal-time errors are best-effort.
}
}
if (_pollTask is not null) if (_pollTask is not null)
{ {
try try
@@ -335,6 +344,17 @@
} }
} }
if (_providerStatusTask is not null)
{
try
{
await _providerStatusTask;
}
catch (OperationCanceledException)
{
}
}
_cts.Dispose(); _cts.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }