Merge rows 80+73+71: nullable LocalDb health fields on unreplicated sites (Host supplies ReplicationIsConfigured), StartupReconcilerActive on the health report (proto field 29) with central No-self-heal badge, standby active-node ERR downgraded via narrowly-keyed Serilog filter

# Conflicts:
#	CLAUDE.md
This commit is contained in:
Joseph Doherty
2026-08-16 04:17:40 -04:00
22 changed files with 1208 additions and 69 deletions
@@ -14,6 +14,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -443,6 +444,120 @@ public class HealthPageTests : BunitContext
Assert.Contains("LocalDb Replication Backlog", cut.Markup));
}
// ----- Register row 73: the "No self-heal" badge -----------------------------------
//
// Tri-state, and only the explicit false is badged. Null means "not reported" — the
// central self-report row, and every site node on a pre-row-73 build — so badging null
// would light up the whole fleet the moment central is upgraded ahead of the sites.
[Fact]
public void Renders_StartupReconcilerBadge_WhenTheSiteReportsItIsMissing()
{
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = true,
LastHeartbeatAt = DateTimeOffset.UtcNow,
LatestReport = ReportWithReconciler("site-a", active: false),
});
var cut = RenderHealthPage();
cut.WaitForAssertion(() =>
Assert.Contains("data-test=\"startup-reconciler-badge\"", cut.Markup));
}
[Fact]
public void DoesNotRender_StartupReconcilerBadge_WhenTheReconcilerIsRunning()
{
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = true,
LastHeartbeatAt = DateTimeOffset.UtcNow,
LatestReport = ReportWithReconciler("site-a", active: true),
});
var cut = RenderHealthPage();
cut.WaitForAssertion(() => Assert.Contains("site-a", cut.Markup));
Assert.DoesNotContain("data-test=\"startup-reconciler-badge\"", cut.Markup);
}
[Fact]
public void DoesNotRender_StartupReconcilerBadge_WhenNotReported()
{
// Null = "not reported", never "missing". This is central's own self-report row and
// any site that has not yet been upgraded.
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = true,
LastHeartbeatAt = DateTimeOffset.UtcNow,
LatestReport = ReportWithReconciler("site-a", active: null),
});
var cut = RenderHealthPage();
cut.WaitForAssertion(() => Assert.Contains("site-a", cut.Markup));
Assert.DoesNotContain("data-test=\"startup-reconciler-badge\"", cut.Markup);
}
[Fact]
public void DoesNotRender_StartupReconcilerBadge_WhenTheSiteHasNoReportYet()
{
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = true,
LastHeartbeatAt = DateTimeOffset.UtcNow,
LatestReport = null,
});
var cut = RenderHealthPage();
cut.WaitForAssertion(() => Assert.Contains("site-a", cut.Markup));
Assert.DoesNotContain("data-test=\"startup-reconciler-badge\"", cut.Markup);
}
[Fact]
public void DoesNotRender_StartupReconcilerBadge_ForAnOfflineSite()
{
// Same rule as the replication-backlog badge: the Offline badge is the signal that
// matters, and stacking a stale warning on top of it is noise.
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = false,
LastHeartbeatAt = DateTimeOffset.UtcNow.AddMinutes(-30),
LatestReport = ReportWithReconciler("site-a", active: false),
});
var cut = RenderHealthPage();
cut.WaitForAssertion(() => Assert.Contains("site-a", cut.Markup));
Assert.DoesNotContain("data-test=\"startup-reconciler-badge\"", cut.Markup);
}
private static SiteHealthReport ReportWithReconciler(string siteId, bool? active) =>
new(
SiteId: siteId,
SequenceNumber: 1,
ReportTimestamp: DateTimeOffset.UtcNow,
DataConnectionStatuses:
new Dictionary<string, Commons.Types.Enums.ConnectionHealth>(),
TagResolutionCounts: new Dictionary<string, TagResolutionStatus>(),
ScriptErrorCount: 0,
AlarmEvaluationErrorCount: 0,
StoreAndForwardBufferDepths: new Dictionary<string, int>(),
DeadLetterCount: 0,
DeployedInstanceCount: 0,
EnabledInstanceCount: 0,
DisabledInstanceCount: 0)
{
StartupReconcilerActive = active,
};
[Fact]
public void Renders_OfflineSince_ForOfflineSiteWithStatusChange()
{