@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

Alarms

@HeaderLine()
@_providerStatus.Label
@if (!GatewayOptions.Value.Alarms.Enabled) {
Alarm auto-subscribe is disabled (MxGateway:Alarms:Enabled is false). The dashboard session is not subscribed to any alarm provider, so this list will stay empty. Enable alarms in configuration and restart the gateway.
} @if (!string.IsNullOrWhiteSpace(_queryError)) {
Alarm query failed: @_queryError
}

Filters

Active Alarms

@{ IReadOnlyList rows = FilteredAlarms(); } @if (rows.Count == 0) {
@if (_alarms.Count == 0) { No alarms are currently Active or ActiveAcked. } else { No alarms match the current filters. }
} else {
@foreach (DashboardActiveAlarm alarm in rows) { }
State Severity Alarm Reference Source Type Area Last Transition Operator
@StateText(alarm.State) @alarm.Severity @alarm.Reference @if (!string.IsNullOrWhiteSpace(alarm.Description)) {
@alarm.Description
}
@DashboardDisplay.Text(alarm.Source) @DashboardDisplay.Text(alarm.AlarmType) @DashboardDisplay.Text(alarm.Area) @(alarm.LastTransition is { } ts ? DashboardDisplay.DateTime(ts) : "-") @DashboardDisplay.Text(alarm.OperatorUser) @if (!string.IsNullOrWhiteSpace(alarm.OperatorComment)) {
@alarm.OperatorComment
}
}
Cleared alarms are not retained — this list reflects only alarms currently Active or ActiveAcked, refreshed every 3 seconds.
@code { private readonly List _alarms = []; private string? _queryError; private int? _workerPid; private DateTimeOffset? _lastRefresh; private int _unackedCount; private int _ackedCount; private bool _showActive = true; private bool _showAcked; private string _areaFilter = string.Empty; private int _minSeverity; private int _maxSeverity = 1000; private string _search = string.Empty; 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() { string refreshed = _lastRefresh is { } at ? $"refreshed {DashboardDisplay.DateTime(at)}" : "awaiting first refresh"; return _workerPid is int pid ? $"{refreshed} · worker pid {pid}" : refreshed; } private IReadOnlyList Areas() { return _alarms .Select(alarm => alarm.Area) .Where(area => !string.IsNullOrWhiteSpace(area)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(area => area, StringComparer.OrdinalIgnoreCase) .ToArray(); } private IReadOnlyList FilteredAlarms() { string query = _search.Trim(); return _alarms .Where(MatchesState) .Where(alarm => _areaFilter.Length == 0 || string.Equals(alarm.Area, _areaFilter, StringComparison.OrdinalIgnoreCase)) .Where(alarm => alarm.Severity >= _minSeverity && alarm.Severity <= _maxSeverity) .Where(alarm => query.Length == 0 || alarm.Reference.Contains(query, StringComparison.OrdinalIgnoreCase) || alarm.Source.Contains(query, StringComparison.OrdinalIgnoreCase) || alarm.Description.Contains(query, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(alarm => alarm.Severity) .ThenByDescending(alarm => alarm.LastTransition ?? DateTimeOffset.MinValue) .ToArray(); } private bool MatchesState(DashboardActiveAlarm alarm) { return alarm.State switch { AlarmConditionState.Active => _showActive, AlarmConditionState.ActiveAcked => _showAcked, _ => true, }; } private static string StateText(AlarmConditionState state) { return state switch { AlarmConditionState.Active => "Active", AlarmConditionState.ActiveAcked => "Acked", AlarmConditionState.Inactive => "Inactive", _ => "Unknown", }; } private static string StateClass(AlarmConditionState state) { return state switch { AlarmConditionState.Active => "alarm-state-active", AlarmConditionState.ActiveAcked => "alarm-state-acked", _ => "alarm-state-other", }; } private async Task PollLoopAsync() { try { await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false); using PeriodicTimer timer = new(TimeSpan.FromSeconds(3)); while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false)) { await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false); } } catch (OperationCanceledException) { } } private async Task RefreshAlarmsAsync() { DashboardAlarmQueryResult result = await LiveData.QueryAlarmsAsync(_cts.Token); _queryError = result.Error; _workerPid = result.WorkerProcessId; _lastRefresh = DateTimeOffset.UtcNow; _alarms.Clear(); _alarms.AddRange(result.Alarms); _unackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.Active); _ackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.ActiveAcked); StateHasChanged(); } /// 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 { await _pollTask; } catch (OperationCanceledException) { } } _cts.Dispose(); GC.SuppressFinalize(this); } }