diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs index bf22aa9..6cf9453 100644 --- a/src/ScadaLink.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs +++ b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs @@ -62,6 +62,15 @@ public interface INotificationOutboxRepository Task ComputeKpisAsync( DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default); + /// + /// Computes a point-in-time per source site. + /// Sites with no notification rows at all are omitted. The stuck and delivered cutoffs + /// are supplied by the caller; the current time used for OldestPendingAge is + /// captured inside the method. + /// + Task> ComputePerSiteKpisAsync( + DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default); + /// /// Persists pending changes tracked on the underlying context. Use this when staging /// multiple changes for a single commit; the individual mutating methods on this diff --git a/src/ScadaLink.Commons/Types/Notifications/SiteNotificationKpiSnapshot.cs b/src/ScadaLink.Commons/Types/Notifications/SiteNotificationKpiSnapshot.cs new file mode 100644 index 0000000..1aa607c --- /dev/null +++ b/src/ScadaLink.Commons/Types/Notifications/SiteNotificationKpiSnapshot.cs @@ -0,0 +1,27 @@ +namespace ScadaLink.Commons.Types.Notifications; + +/// +/// Point-in-time notification-outbox metrics scoped to a single source site. +/// The per-site counterpart of ; surfaced +/// in the per-site breakdown table on the Notification KPIs page. +/// +/// The site identifier these metrics are scoped to. +/// Count of this site's non-terminal rows (Pending + Retrying). +/// +/// Count of this site's non-terminal rows whose CreatedAt is older than the stuck cutoff. +/// +/// Count of this site's rows in the Parked status. +/// +/// Count of this site's Delivered rows whose DeliveredAt is at or after the +/// "delivered since" timestamp. +/// +/// +/// Age of this site's oldest non-terminal row, or null when it has none. +/// +public record SiteNotificationKpiSnapshot( + string SourceSiteId, + int QueueDepth, + int StuckCount, + int ParkedCount, + int DeliveredLastInterval, + TimeSpan? OldestPendingAge); diff --git a/tests/ScadaLink.Commons.Tests/Types/SiteNotificationKpiSnapshotTests.cs b/tests/ScadaLink.Commons.Tests/Types/SiteNotificationKpiSnapshotTests.cs new file mode 100644 index 0000000..3e3550c --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Types/SiteNotificationKpiSnapshotTests.cs @@ -0,0 +1,32 @@ +using ScadaLink.Commons.Types.Notifications; + +namespace ScadaLink.Commons.Tests.Types; + +public class SiteNotificationKpiSnapshotTests +{ + [Fact] + public void Constructor_AssignsAllMembers() + { + var snapshot = new SiteNotificationKpiSnapshot( + SourceSiteId: "plant-a", + QueueDepth: 5, + StuckCount: 2, + ParkedCount: 1, + DeliveredLastInterval: 40, + OldestPendingAge: TimeSpan.FromMinutes(12)); + + Assert.Equal("plant-a", snapshot.SourceSiteId); + Assert.Equal(5, snapshot.QueueDepth); + Assert.Equal(2, snapshot.StuckCount); + Assert.Equal(1, snapshot.ParkedCount); + Assert.Equal(40, snapshot.DeliveredLastInterval); + Assert.Equal(TimeSpan.FromMinutes(12), snapshot.OldestPendingAge); + } + + [Fact] + public void OldestPendingAge_IsNullableForSitesWithNoBacklog() + { + var snapshot = new SiteNotificationKpiSnapshot("plant-b", 0, 0, 0, 0, null); + Assert.Null(snapshot.OldestPendingAge); + } +}