feat(ui): Audit KPI tiles on Health dashboard (#23 M7)

Adds three KPI tiles to the central Health dashboard for the Audit channel:
volume (rows in the last hour), error rate (Failed/Parked/Discarded over
total), and backlog (sum of SiteAuditBacklog.PendingCount across all sites).

Repo + service:
- IAuditLogRepository.GetKpiSnapshotAsync(window, nowUtc) — single aggregate
  SELECT over the trailing window returning total + error counts; nowUtc is
  optional for production callers and pinned by integration tests against the
  shared MSSQL fixture so the global counts are deterministic.
- AuditLogQueryService.GetKpiSnapshotAsync() — composes the repo aggregate
  with a sum of SiteAuditBacklog.PendingCount read from ICentralHealthAggregator.
- AuditLogKpiSnapshot record in Commons/Types/.

UI:
- New AuditKpiTiles Blazor component (Components/Health/) — three Bootstrap
  card-tiles, click navigates to /audit/log with the matching pre-filter.
- Health.razor wires the tiles in alongside the existing Notification Outbox
  KPIs; LoadAuditKpis() runs on every 10s refresh tick and degrades to em
  dashes + inline error if the query fails.
- AuditLogPage extended to parse ?status= so the error-rate tile drill-in
  (?status=Failed) auto-loads the grid.

Tests:
- AuditLogRepositoryTests: GetKpiSnapshotAsync mixed-status + empty-window
  cases against the MSSQL migration fixture.
- AuditLogQueryServiceTests: forwarding + backlog composition; sites with
  null SiteAuditBacklog contribute zero.
- AuditKpiTilesTests: 9 bUnit tests covering tile render, error-rate maths
  with safe zero-events handling, em-dash unavailable path, click-through
  navigation, and warning/danger border thresholds.
- HealthPageTests: new Renders_AuditKpiTiles_WithValues plus IAuditLogQueryService
  stub registration in the constructor so existing outbox tests still pass.
- AuditLogPageScaffoldTests: ?status=Failed auto-load + unknown status drop.
This commit is contained in:
Joseph Doherty
2026-05-20 20:43:57 -04:00
parent 38fc9b4102
commit 943c2ced39
18 changed files with 969 additions and 7 deletions

View File

@@ -1,5 +1,8 @@
@page "/monitoring/health"
@attribute [Authorize]
@using ScadaLink.CentralUI.Components.Health
@using ScadaLink.CentralUI.Services
@using ScadaLink.Commons.Types
@using ScadaLink.Commons.Types.Enums
@using ScadaLink.Commons.Entities.Sites
@using ScadaLink.Commons.Interfaces.Repositories
@@ -10,6 +13,7 @@
@inject ICentralHealthAggregator HealthAggregator
@inject ISiteRepository SiteRepository
@inject CommunicationService CommunicationService
@inject IAuditLogQueryService AuditLogQueryService
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
@@ -56,6 +60,12 @@
<div class="text-muted small mb-3">Notification Outbox KPIs unavailable: @_outboxKpiError</div>
}
@* Audit Log (#23) M7 Bundle E — three KPI tiles for the Audit channel
(volume / error rate / backlog). Refreshed alongside the site states. *@
<AuditKpiTiles Snapshot="@_auditKpi"
IsAvailable="@_auditKpiAvailable"
ErrorMessage="@_auditKpiError" />
@if (_siteStates.Count == 0)
{
<div class="alert alert-info">No site health reports received yet.</div>
@@ -347,6 +357,13 @@
private bool _outboxKpiAvailable;
private string? _outboxKpiError;
// Audit Log (#23) M7 Bundle E — Audit KPI tiles. Volume + error rate come
// from a 1h aggregate over the central AuditLog table; backlog sums the
// per-site SiteAuditBacklog.PendingCount via the health aggregator.
private AuditLogKpiSnapshot? _auditKpi;
private bool _auditKpiAvailable;
private string? _auditKpiError;
private static bool SiteHasActiveErrors(SiteHealthState state)
{
var report = state.LatestReport;
@@ -384,6 +401,7 @@
{
_siteStates = HealthAggregator.GetAllSiteStates();
await LoadOutboxKpis();
await LoadAuditKpis();
}
private async Task LoadOutboxKpis()
@@ -416,6 +434,24 @@
private string OutboxTileValue(int value) =>
_outboxKpiAvailable ? value.ToString() : "—";
// Audit KPI loader: wraps the service call so a transient DB outage degrades
// the three tiles to em dashes with an inline error rather than killing the
// dashboard. Mirrors LoadOutboxKpis's error handling shape.
private async Task LoadAuditKpis()
{
try
{
_auditKpi = await AuditLogQueryService.GetKpiSnapshotAsync();
_auditKpiAvailable = true;
_auditKpiError = null;
}
catch (Exception ex)
{
_auditKpiAvailable = false;
_auditKpiError = $"KPI query failed: {ex.Message}";
}
}
private string GetSiteName(string siteId)
{
return _siteNames.GetValueOrDefault(siteId, siteId);