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:
@@ -19,8 +19,10 @@ namespace ScadaLink.CentralUI.Components.Pages.Audit;
|
||||
/// Bundle D (M7-T10..T12) adds query-string drill-in parsing so other pages can
|
||||
/// deep-link to a pre-filtered Audit Log: <c>?correlationId=</c>, <c>?target=</c>,
|
||||
/// <c>?actor=</c>, <c>?site=</c>, <c>?channel=</c>, and the UI-only
|
||||
/// <c>?instance=</c> are read on initialization. When any param is present we
|
||||
/// allocate a fresh <see cref="AuditLogQueryFilter"/> and assign it to
|
||||
/// <c>?instance=</c> are read on initialization. Bundle E (M7-T13) extends
|
||||
/// this with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
|
||||
/// drill in to <c>?status=Failed</c>. When any param is present we allocate a
|
||||
/// fresh <see cref="AuditLogQueryFilter"/> and assign it to
|
||||
/// <see cref="_currentFilter"/>, which kicks the results grid into auto-load
|
||||
/// without the user clicking Apply. Unknown values (e.g. an invalid enum name)
|
||||
/// are silently dropped — the page still renders, just without that constraint.
|
||||
@@ -94,6 +96,17 @@ public partial class AuditLogPage
|
||||
channel = parsedChannel;
|
||||
}
|
||||
|
||||
// Bundle E (M7-T13): the Health-dashboard Audit error-rate tile drills in
|
||||
// with ?status=Failed (and operators may craft URLs with Parked/Discarded).
|
||||
// Unknown values are silently dropped — the page still renders without
|
||||
// the constraint.
|
||||
AuditStatus? status = null;
|
||||
if (query.TryGetValue("status", out var statusValues)
|
||||
&& Enum.TryParse<AuditStatus>(statusValues.ToString(), ignoreCase: true, out var parsedStatus))
|
||||
{
|
||||
status = parsedStatus;
|
||||
}
|
||||
|
||||
// Instance is UI-only — the filter contract has no matching column, so we
|
||||
// pass it as a separate seam to the filter bar.
|
||||
if (query.TryGetValue("instance", out var instanceValues))
|
||||
@@ -109,13 +122,14 @@ public partial class AuditLogPage
|
||||
// auto-loads. Pure ?instance= deep links (UI-only) do not trigger auto-load
|
||||
// because the filter contract has no instance column — the user still needs
|
||||
// to refine + Apply for those.
|
||||
if (correlationId is null && target is null && actor is null && site is null && channel is null)
|
||||
if (correlationId is null && target is null && actor is null && site is null && channel is null && status is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentFilter = new AuditLogQueryFilter(
|
||||
Channel: channel,
|
||||
Status: status,
|
||||
SourceSiteId: site,
|
||||
Target: target,
|
||||
Actor: actor,
|
||||
|
||||
Reference in New Issue
Block a user