Files
ScadaBridge/src/ScadaLink.CentralUI/Services/IAuditLogQueryService.cs
T
Joseph Doherty 943c2ced39 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.
2026-05-20 20:43:57 -04:00

54 lines
2.5 KiB
C#

using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Audit;
namespace ScadaLink.CentralUI.Services;
/// <summary>
/// CentralUI facade over <see cref="ScadaLink.Commons.Interfaces.Repositories.IAuditLogRepository"/>
/// (#23 M7-T3). The Audit Log page's results grid talks to this service rather than
/// the repository directly so tests can substitute a fake without spinning up EF
/// Core, and so a future caching / shaping layer (e.g. server-side CSV streaming)
/// can hang off the same seam.
/// </summary>
public interface IAuditLogQueryService
{
/// <summary>
/// Returns a keyset-paged result page for <paramref name="filter"/>. When
/// <paramref name="paging"/> is <c>null</c>, defaults to <see cref="DefaultPageSize"/>
/// rows with no cursor (first page). The repository orders by
/// <c>(OccurredAtUtc DESC, EventId DESC)</c>; pass the last row's
/// <see cref="AuditEvent.OccurredAtUtc"/> + <see cref="AuditEvent.EventId"/>
/// back as the cursor for the next page.
/// </summary>
Task<IReadOnlyList<AuditEvent>> QueryAsync(
AuditLogQueryFilter filter,
AuditLogPaging? paging = null,
CancellationToken ct = default);
/// <summary>Default page size when callers don't specify one.</summary>
int DefaultPageSize { get; }
/// <summary>
/// Audit Log (#23) M7 Bundle E (T13) — returns the point-in-time KPI snapshot
/// the Health dashboard's Audit tiles render. Composes:
/// <list type="bullet">
/// <item><c>TotalEventsLastHour</c> + <c>ErrorEventsLastHour</c> from
/// <see cref="ScadaLink.Commons.Interfaces.Repositories.IAuditLogRepository.GetKpiSnapshotAsync"/>
/// (1-hour trailing window).</item>
/// <item><c>BacklogTotal</c> from the sum of every site's
/// <c>SiteHealthReport.SiteAuditBacklog.PendingCount</c> via
/// <see cref="ScadaLink.HealthMonitoring.ICentralHealthAggregator"/>.</item>
/// </list>
/// </summary>
/// <remarks>
/// Repository + aggregator are read independently; if either source has no
/// data the corresponding field is zero (a real signal — "no events" vs
/// "no backlog" — rather than an error). The service does NOT swallow
/// exceptions; the page wraps the call in a try/catch so a transient DB
/// outage degrades the tile group to "unavailable" rather than killing the
/// dashboard.
/// </remarks>
Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(CancellationToken ct = default);
}