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"
@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> GatewayOptions
@inject DashboardHubConnectionFactory HubFactory
@inject IGatewayAlarmService AlarmService
<PageTitle>Dashboard Alarms</PageTitle>
@@ -173,13 +172,13 @@
private Task? _pollTask;
private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
private HubConnection? _alarmsHub;
private Task? _providerStatusTask;
/// <inheritdoc />
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<AlarmFeedMessage>(AlarmsHub.AlarmMessage, async message =>
while (!_cts.IsCancellationRequested)
{
if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
try
{
await foreach (AlarmFeedMessage message in AlarmService
.StreamAsync(alarmFilterPrefix: null, _cts.Token)
.ConfigureAwait(false))
{
if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
{
continue;
}
_providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
}
});
try
}
catch (OperationCanceledException)
{
await _alarmsHub.StartAsync(_cts.Token).ConfigureAwait(false);
return;
}
catch
{
// The badge is best-effort; it stays at the healthy default until
// the hub reconnects and delivers a fresh provider-status message.
// 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);
}