feat(sitecallaudit): query, KPI and detail backend for the Site Calls page

This commit is contained in:
Joseph Doherty
2026-05-21 04:14:49 -04:00
parent 6f0d2ca499
commit e3519fdb39
17 changed files with 1514 additions and 18 deletions

View File

@@ -338,6 +338,104 @@ public class SiteCallAuditRepositoryTests : IClassFixture<MsSqlMigrationFixture>
Assert.NotNull(await repo.GetAsync(recentTerminalId));
}
// --- KPI snapshot tests -------------------------------------------------
[SkippableFact]
public async Task ComputeKpisAsync_CountsBufferedParkedFailedDeliveredAndStuck()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var site = NewSiteId();
await using var context = CreateContext();
var repo = new SiteCallAuditRepository(context);
var now = DateTime.UtcNow;
var stuckCutoff = now.AddMinutes(-10);
var intervalSince = now.AddHours(-1);
// Buffered + stuck (non-terminal Attempted, created 30 min ago).
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Attempted", createdAtUtc: now.AddMinutes(-30)));
// Buffered but NOT stuck (non-terminal Attempted, created 2 min ago).
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Attempted", createdAtUtc: now.AddMinutes(-2)));
// Parked (terminal).
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Parked",
createdAtUtc: now.AddMinutes(-5), updatedAtUtc: now.AddMinutes(-4),
terminal: true, terminalAtUtc: now.AddMinutes(-4)));
// Delivered within the interval.
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Delivered",
createdAtUtc: now.AddMinutes(-4), updatedAtUtc: now.AddMinutes(-1),
terminal: true, terminalAtUtc: now.AddMinutes(-1)));
// Failed within the interval.
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Failed",
createdAtUtc: now.AddMinutes(-6), updatedAtUtc: now.AddMinutes(-2),
terminal: true, terminalAtUtc: now.AddMinutes(-2)));
// Delivered OUTSIDE the interval (2 hours ago) — must not count.
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), site, status: "Delivered",
createdAtUtc: now.AddHours(-3), updatedAtUtc: now.AddHours(-2),
terminal: true, terminalAtUtc: now.AddHours(-2)));
var snapshot = await repo.ComputeKpisAsync(stuckCutoff, intervalSince);
// Counts are global; assert the floor since the table is shared with
// other tests. The OUTSIDE-interval Delivered row proves the window
// bounds the throughput counts.
Assert.True(snapshot.BufferedCount >= 2);
Assert.True(snapshot.ParkedCount >= 1);
Assert.True(snapshot.StuckCount >= 1);
Assert.True(snapshot.DeliveredLastInterval >= 1);
Assert.True(snapshot.FailedLastInterval >= 1);
Assert.NotNull(snapshot.OldestPendingAge);
Assert.True(snapshot.OldestPendingAge >= TimeSpan.FromMinutes(25));
}
[SkippableFact]
public async Task ComputePerSiteKpisAsync_ScopesCountsToEachSite()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var siteA = NewSiteId();
var siteB = NewSiteId();
await using var context = CreateContext();
var repo = new SiteCallAuditRepository(context);
var now = DateTime.UtcNow;
var stuckCutoff = now.AddMinutes(-10);
var intervalSince = now.AddHours(-1);
// siteA: 2 buffered (one stuck), 1 parked.
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteA, status: "Attempted", createdAtUtc: now.AddMinutes(-30)));
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteA, status: "Attempted", createdAtUtc: now.AddMinutes(-2)));
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), siteA, status: "Parked",
createdAtUtc: now.AddMinutes(-5), updatedAtUtc: now.AddMinutes(-4),
terminal: true, terminalAtUtc: now.AddMinutes(-4)));
// siteB: 1 delivered within interval only.
await repo.UpsertAsync(NewRow(
TrackedOperationId.New(), siteB, status: "Delivered",
createdAtUtc: now.AddMinutes(-4), updatedAtUtc: now.AddMinutes(-1),
terminal: true, terminalAtUtc: now.AddMinutes(-1)));
var perSite = await repo.ComputePerSiteKpisAsync(stuckCutoff, intervalSince);
var a = Assert.Single(perSite, s => s.SourceSite == siteA);
Assert.Equal(2, a.BufferedCount);
Assert.Equal(1, a.ParkedCount);
Assert.Equal(1, a.StuckCount);
Assert.NotNull(a.OldestPendingAge);
var b = Assert.Single(perSite, s => s.SourceSite == siteB);
Assert.Equal(0, b.BufferedCount);
Assert.Equal(1, b.DeliveredLastInterval);
// siteB has no non-terminal rows — no oldest-pending age.
Assert.Null(b.OldestPendingAge);
}
// --- helpers ------------------------------------------------------------
private ScadaLinkDbContext CreateContext()