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
@@ -285,6 +285,14 @@
Secret mirror failing
</span>
}
@if (HasAuditRejectedRows(state))
{
<span class="badge bg-warning text-dark"
data-test="audit-rejected-badge"
title="Central permanently refused @state.LatestReport!.SiteAuditBacklog!.RejectedCount site audit row(s) as un-insertable. They are still served by the reconciliation pull, but they indicate a row-intrinsic insert failure at central — check the central audit ingest logs.">
Audit rows rejected
</span>
}
@if (!state.IsOnline && state.LastStatusChangeAt is { } changedAt)
{
<small class="text-muted">offline since @changedAt.ToString("u")</small>
@@ -629,6 +637,13 @@
&& state.LatestReport?.SecretMirrorStatuses is { } statuses
&& statuses.Any(n => n.Failures.Count > 0);
// Gitea #69: warn when central has permanently refused site audit rows. Null backlog
// ("no data": central self-report, pre-#69 site) and zero are healthy; offline sites are
// skipped like the other warning badges.
private static bool HasAuditRejectedRows(SiteHealthState state) =>
state.IsOnline
&& state.LatestReport?.SiteAuditBacklog is { RejectedCount: > 0 };
// Online central nodes, from the same ClusterNodes list the Nodes column renders.
// Fewer than two means no standby, which disables the manual-failover control. This is
// a display-side guard only — AkkaManualFailoverService re-checks against live cluster