@page "/notifications/kpis" @attribute [Authorize(Policy = ZB.MOM.WW.ScadaBridge.Security.AuthorizationPolicies.RequireDeployment)] @using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites @using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories @using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification @using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications @using ZB.MOM.WW.ScadaBridge.Communication @inject CommunicationService CommunicationService @inject ISiteRepository SiteRepository @inject ILogger Logger

Notification KPIs

@* ── Global KPI tiles ── *@ @if (_kpiError != null) {
KPIs unavailable: @_kpiError
} else {

@_kpi.QueueDepth

Queue Depth

@_kpi.StuckCount

Stuck

@_kpi.ParkedCount

Parked

@_kpi.DeliveredLastInterval

Delivered (last interval)

@FormatAge(_kpi.OldestPendingAge)

Oldest Pending Age
} @* ── Per-node breakdown (T6: additive) ── *@
Per-node breakdown
@if (_perNodeError != null) {
Per-node KPIs unavailable: @_perNodeError
} else if (_perNode.Count == 0) {
No per-node activity (rows may have a null SourceNode).
} else {
@foreach (var n in _perNode) { }
Node Queue Depth Stuck Parked Delivered (last interval) Oldest Pending Age
@n.SourceNode @n.QueueDepth @n.StuckCount @n.ParkedCount @n.DeliveredLastInterval @FormatAge(n.OldestPendingAge)
} @* ── Per-site breakdown ── *@
Per-site breakdown
@if (_perSiteError != null) {
Per-site KPIs unavailable: @_perSiteError
} else if (_perSite.Count == 0) {
No per-site activity.
} else {
@foreach (var s in _perSite) { }
Site Queue Depth Stuck Parked Delivered (last interval) Oldest Pending Age
@SiteName(s.SourceSiteId) @s.QueueDepth @s.StuckCount @s.ParkedCount @s.DeliveredLastInterval @FormatAge(s.OldestPendingAge)
}
@code { private List _sites = new(); private NotificationKpiResponse _kpi = new(string.Empty, true, null, 0, 0, 0, 0, null); private string? _kpiError; private IReadOnlyList _perSite = Array.Empty(); private string? _perSiteError; // ── Per-node (T6: M5.2 per-node stuck-count KPIs) ── private IReadOnlyList _perNode = Array.Empty(); private string? _perNodeError; private bool _loading; protected override async Task OnInitializedAsync() { try { _sites = (await SiteRepository.GetAllSitesAsync()).ToList(); } catch (Exception ex) { // Non-fatal — the per-site table falls back to raw site identifiers. Logger.LogWarning(ex, "Failed to load sites for the KPI per-site breakdown."); } await RefreshAll(); } private async Task RefreshAll() { _loading = true; // Race-free despite all tasks mutating component fields: Blazor Server runs // every continuation on the circuit's single-threaded synchronization context. await Task.WhenAll(LoadGlobalKpis(), LoadPerSiteKpis(), LoadPerNodeKpis()); _loading = false; } private async Task LoadGlobalKpis() { try { var response = await CommunicationService.GetNotificationKpisAsync( new NotificationKpiRequest(Guid.NewGuid().ToString("N"))); if (response.Success) { _kpi = response; _kpiError = null; } else { _kpiError = response.ErrorMessage ?? "KPI query failed."; } } catch (Exception ex) { _kpiError = $"KPI query failed: {ex.Message}"; } } private async Task LoadPerSiteKpis() { try { var response = await CommunicationService.GetPerSiteNotificationKpisAsync( new PerSiteNotificationKpiRequest(Guid.NewGuid().ToString("N"))); if (response.Success) { _perSite = response.Sites; _perSiteError = null; } else { _perSiteError = response.ErrorMessage ?? "Per-site KPI query failed."; } } catch (Exception ex) { _perSiteError = $"Per-site KPI query failed: {ex.Message}"; } } private async Task LoadPerNodeKpis() { try { var response = await CommunicationService.GetPerNodeNotificationKpisAsync( new PerNodeNotificationKpiRequest(Guid.NewGuid().ToString("N"))); if (response.Success) { _perNode = response.Nodes; _perNodeError = null; } else { _perNodeError = response.ErrorMessage ?? "Per-node KPI query failed."; } } catch (Exception ex) { _perNodeError = $"Per-node KPI query failed: {ex.Message}"; } } private string SiteName(string siteId) => _sites.FirstOrDefault(s => s.SiteIdentifier == siteId)?.Name ?? siteId; private static string FormatAge(TimeSpan? age) { if (age == null) return "—"; var t = age.Value; if (t.TotalSeconds < 60) return $"{(int)t.TotalSeconds}s"; if (t.TotalMinutes < 60) return $"{(int)t.TotalMinutes}m"; if (t.TotalHours < 24) return $"{(int)t.TotalHours}h"; return $"{(int)t.TotalDays}d"; } }