feat(health): report permanently rejected site audit rows on the site health report (ScadaBridge#69)

SiteAuditBacklogSnapshot gains an optional trailing RejectedCount (gauge of
Rejected forward-state rows, read from the site store), carried on the wire as
SiteAuditBacklogSnapshotDto.rejected_count = 4 (additive; old sites read as 0,
old central ignores it). Health page shows an 'Audit rows rejected' badge.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 12:46:34 -05:00
parent 3ae3f1bcef
commit 0eac956d25
10 changed files with 263 additions and 100 deletions
@@ -26,7 +26,19 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types;
/// invariant (alog.md §10) — a steady file-size growth past the retention
/// window points at a stuck purge or a stuck forwarder.
/// </param>
/// <param name="RejectedCount">
/// Number of rows currently in the <c>Rejected</c> forward state — rows central permanently
/// refused (listed in the ack's <c>rejected_event_ids</c>) on repeated pushes and that the
/// site moved out of the push queue. They are still owed to central via the reconciliation
/// pull (so they are ALSO included in <paramref name="PendingCount"/>), but a non-zero value
/// means central is rejecting rows as un-insertable, which is a data-quality fault worth an
/// operator's attention rather than a drain-lag symptom. A point-in-time gauge read from the
/// site store (survives restarts), unlike the process-local
/// <c>scadabridge.site.audit.rejected_rows</c> counter. Trailing, defaulted member: an older
/// site omits it (reads as 0) and the wire field is optional (Gitea #69).
/// </param>
public sealed record SiteAuditBacklogSnapshot(
int PendingCount,
DateTime? OldestPendingUtc,
long OnDiskBytes);
long OnDiskBytes,
int RejectedCount = 0);