feat(kpi): K13 — Notification Outbox trend charts (T11 first consumer)

This commit is contained in:
Joseph Doherty
2026-06-17 20:29:30 -04:00
parent f0177d5073
commit 0dc819f191
2 changed files with 153 additions and 1 deletions
@@ -4,9 +4,12 @@
@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.Commons.Types.Kpi
@using ZB.MOM.WW.ScadaBridge.Communication
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@inject CommunicationService CommunicationService
@inject ISiteRepository SiteRepository
@inject IKpiHistoryQueryService KpiHistory
@inject ILogger<NotificationKpis> Logger
<div class="container-fluid mt-3">
@@ -158,6 +161,39 @@
</table>
</div>
}
@* ── Trends (T11: first KPI-history consumer) ── *@
<div class="d-flex justify-content-between align-items-center mt-4 mb-2" data-test="notification-trends">
<h5 class="mb-0">Trends</h5>
<div class="btn-group btn-group-sm" role="group" aria-label="Trend window">
<button type="button"
class="btn @(_windowHours == 24 ? "btn-primary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(24)" disabled="@_trendsLoading">24h</button>
<button type="button"
class="btn @(_windowHours == 168 ? "btn-primary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(168)" disabled="@_trendsLoading">7d</button>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Queue Depth"
Points="@_queueDepthSeries"
IsAvailable="@_trendsAvailable"
ErrorMessage="@_trendsError" />
</div>
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Parked"
Points="@_parkedSeries"
IsAvailable="@_trendsAvailable"
ErrorMessage="@_trendsError" />
</div>
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Delivered / interval"
Points="@_deliveredSeries"
IsAvailable="@_trendsAvailable"
ErrorMessage="@_trendsError" />
</div>
</div>
</div>
@code {
@@ -175,6 +211,16 @@
private bool _loading;
// ── Trends (T11: first KPI-history consumer) ──
// Window in hours: 24h (default) or 168h (7d). Toggling re-queries.
private int _windowHours = 24;
private bool _trendsLoading;
private bool _trendsAvailable = true;
private string? _trendsError;
private IReadOnlyList<KpiSeriesPoint>? _queueDepthSeries;
private IReadOnlyList<KpiSeriesPoint>? _parkedSeries;
private IReadOnlyList<KpiSeriesPoint>? _deliveredSeries;
protected override async Task OnInitializedAsync()
{
try
@@ -195,10 +241,55 @@
_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());
await Task.WhenAll(LoadGlobalKpis(), LoadPerSiteKpis(), LoadPerNodeKpis(), LoadTrends());
_loading = false;
}
private async Task SetWindowAsync(int windowHours)
{
if (_windowHours == windowHours)
{
return;
}
_windowHours = windowHours;
await LoadTrends();
}
private async Task LoadTrends()
{
_trendsLoading = true;
try
{
var toUtc = DateTime.UtcNow;
var fromUtc = toUtc - TimeSpan.FromHours(_windowHours);
// Best-effort: one query failure must NOT break the page — on any
// exception the charts fall back to the unavailable placeholder while
// the KPI tiles above stay rendered.
_queueDepthSeries = await GetSeries("queueDepth", fromUtc, toUtc);
_parkedSeries = await GetSeries("parkedCount", fromUtc, toUtc);
_deliveredSeries = await GetSeries("deliveredLastInterval", fromUtc, toUtc);
_trendsAvailable = true;
_trendsError = null;
}
catch (Exception ex)
{
_trendsAvailable = false;
_trendsError = "Trend data unavailable.";
Logger.LogWarning(ex, "Failed to load notification-outbox KPI trend series.");
}
finally
{
_trendsLoading = false;
}
}
private Task<IReadOnlyList<KpiSeriesPoint>> GetSeries(string metric, DateTime fromUtc, DateTime toUtc) =>
KpiHistory.GetSeriesAsync(
KpiSources.NotificationOutbox, metric, KpiScopes.Global, scopeKey: null, fromUtc, toUtc);
private async Task LoadGlobalKpis()
{
try