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 10a9f60..93180af 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,7 +1,10 @@ @page "/alarms" @implements IAsyncDisposable +@using Microsoft.AspNetCore.SignalR.Client +@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs @inject IDashboardLiveDataService LiveData @inject IOptions GatewayOptions +@inject DashboardHubConnectionFactory HubFactory Dashboard Alarms @@ -10,6 +13,12 @@

Alarms

@HeaderLine()
+
+ + @_providerStatus.Label + +
@if (!GatewayOptions.Value.Alarms.Enabled) @@ -163,10 +172,44 @@ private readonly CancellationTokenSource _cts = new(); private Task? _pollTask; + private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy; + private HubConnection? _alarmsHub; + /// protected override void OnInitialized() { _pollTask = PollLoopAsync(); + _ = AttachAlarmsHubAsync(); + } + + private string? ProviderStatusTitle() + { + return _providerStatus.IsDegraded && !string.IsNullOrWhiteSpace(_providerStatus.Reason) + ? _providerStatus.Reason + : null; + } + + private async Task AttachAlarmsHubAsync() + { + _alarmsHub = HubFactory.Create("/hubs/alarms"); + _alarmsHub.On(AlarmsHub.AlarmMessage, async message => + { + if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus) + { + _providerStatus = DashboardAlarmProviderStatus.FromFeed(message); + await InvokeAsync(StateHasChanged).ConfigureAwait(false); + } + }); + + 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. + } } private string HeaderLine() @@ -268,6 +311,19 @@ public async ValueTask DisposeAsync() { 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 diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs new file mode 100644 index 0000000..37251b0 --- /dev/null +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs @@ -0,0 +1,78 @@ +using ZB.MOM.WW.MxGateway.Contracts.Proto; + +namespace ZB.MOM.WW.MxGateway.Server.Dashboard; + +/// +/// Dashboard projection of an message +/// carried on the alarm feed. Maps the protobuf provider mode / degraded +/// flag into Bootstrap-only display fields so the Alarms page can render a +/// status badge without touching protobuf types. +/// +public sealed record DashboardAlarmProviderStatus( + AlarmProviderMode Mode, + bool IsDegraded, + string Label, + string BadgeCssClass, + string Reason, + DateTimeOffset? SinceUtc) +{ + /// Badge label shown when the alarm-manager provider is healthy. + public const string AlarmManagerLabel = "Alarm Manager"; + + /// Badge label shown when the feed has fallen back to subtag monitoring. + public const string DegradedLabel = "Subtag monitoring (degraded)"; + + private const string HealthyBadge = "bg-success"; + private const string DegradedBadge = "bg-warning text-dark"; + + /// + /// The default status assumed before the first provider-status message + /// arrives: healthy alarm-manager mode. + /// + public static DashboardAlarmProviderStatus Healthy { get; } = new( + Mode: AlarmProviderMode.Alarmmgr, + IsDegraded: false, + Label: AlarmManagerLabel, + BadgeCssClass: HealthyBadge, + Reason: string.Empty, + SinceUtc: null); + + /// Projects an alarm-feed provider-status payload into a dashboard badge model. + /// The provider-status payload from an . + /// The projected dashboard status. + public static DashboardAlarmProviderStatus FromProviderStatus(AlarmProviderStatus status) + { + ArgumentNullException.ThrowIfNull(status); + + // Treat the explicit degraded flag and the SUBTAG mode as equivalent; + // the contract sets degraded=true whenever mode == SUBTAG, but guard + // against either being set independently. + bool degraded = status.Degraded || status.Mode == AlarmProviderMode.Subtag; + + return new DashboardAlarmProviderStatus( + Mode: status.Mode, + IsDegraded: degraded, + Label: degraded ? DegradedLabel : AlarmManagerLabel, + BadgeCssClass: degraded ? DegradedBadge : HealthyBadge, + Reason: status.Reason ?? string.Empty, + SinceUtc: status.Since?.ToDateTimeOffset()); + } + + /// Projects an alarm-feed message into a dashboard badge model. + /// An alarm-feed message whose payload is a provider status. + /// The projected dashboard status. + /// The message does not carry a provider-status payload. + public static DashboardAlarmProviderStatus FromFeed(AlarmFeedMessage message) + { + ArgumentNullException.ThrowIfNull(message); + + if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus) + { + throw new ArgumentException( + "Alarm-feed message does not carry a provider-status payload.", + nameof(message)); + } + + return FromProviderStatus(message.ProviderStatus); + } +} diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs index ee00eee..46a7d29 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs @@ -137,6 +137,41 @@ public sealed class DashboardBrowseAndAlarmModelTests Assert.False(ackedRow.IsUnacknowledged); } + /// Verifies that a healthy alarmmgr provider status maps to a green badge. + [Fact] + public void FromProviderStatus_Alarmmgr_NotDegraded_GreenBadge() + { + AlarmProviderStatus status = new() + { + Mode = AlarmProviderMode.Alarmmgr, + Degraded = false, + }; + + DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); + + Assert.False(model.IsDegraded); + Assert.Contains("bg-success", model.BadgeCssClass, StringComparison.Ordinal); + Assert.Equal(DashboardAlarmProviderStatus.AlarmManagerLabel, model.Label); + } + + /// Verifies that a degraded subtag provider status maps to an amber warning badge. + [Fact] + public void FromProviderStatus_Subtag_Degraded_WarningBadge() + { + AlarmProviderStatus status = new() + { + Mode = AlarmProviderMode.Subtag, + Degraded = true, + Reason = "x", + }; + + DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); + + Assert.True(model.IsDegraded); + Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal); + Assert.Equal("x", model.Reason); + } + /// Verifies that the formatter renders array elements and element type correctly. [Fact] public void FormatValue_AndDataType_RenderArrayElementsAndElementType()