feat(audit): M5.2 per-node stuck-count KPIs (T6) — repo per-node aggregation, actor message pair, CentralUI tiles

This commit is contained in:
Joseph Doherty
2026-06-16 21:34:14 -04:00
parent a07ff28f10
commit 209f368cb5
25 changed files with 840 additions and 6 deletions
@@ -122,6 +122,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
Receive<DiscardNotificationRequest>(HandleDiscard);
Receive<NotificationKpiRequest>(HandleKpiRequest);
Receive<PerSiteNotificationKpiRequest>(HandlePerSiteKpiRequest);
Receive<PerNodeNotificationKpiRequest>(HandlePerNodeKpiRequest);
}
/// <inheritdoc />
@@ -1081,6 +1082,38 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
return new PerSiteNotificationKpiResponse(correlationId, Success: true, ErrorMessage: null, sites);
}
/// <summary>
/// Handles a per-node KPI request, computing the per-source-node outbox metrics with the
/// same stuck cutoff and delivered window as <see cref="HandleKpiRequest"/>. Additive
/// alongside <see cref="HandlePerSiteKpiRequest"/> — does not change per-site behaviour.
/// </summary>
private void HandlePerNodeKpiRequest(PerNodeNotificationKpiRequest request)
{
var sender = Sender;
var now = DateTimeOffset.UtcNow;
var stuckCutoff = StuckCutoff(now);
var deliveredSince = now - _options.DeliveredKpiWindow;
ComputePerNodeKpisAsync(request.CorrelationId, stuckCutoff, deliveredSince).PipeTo(
sender,
success: response => response,
failure: ex => new PerNodeNotificationKpiResponse(
request.CorrelationId,
Success: false,
ErrorMessage: ex.GetBaseException().Message,
Nodes: Array.Empty<NodeNotificationKpiSnapshot>()));
}
private async Task<PerNodeNotificationKpiResponse> ComputePerNodeKpisAsync(
string correlationId, DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince)
{
using var scope = _serviceProvider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>();
var nodes = await repository.ComputePerNodeKpisAsync(stuckCutoff, deliveredSince);
return new PerNodeNotificationKpiResponse(correlationId, Success: true, ErrorMessage: null, nodes);
}
/// <summary>
/// The instant before which a still-pending notification counts as stuck — <paramref name="now"/>
/// offset back by <see cref="NotificationOutboxOptions.StuckAgeThreshold"/>.