feat(ui): health dashboard shows metrics-stale badge and offline-since timestamp

This commit is contained in:
Joseph Doherty
2026-07-08 16:23:43 -04:00
parent c73b7faa11
commit 87c7255912
2 changed files with 83 additions and 0 deletions
@@ -82,6 +82,11 @@ public class HealthPageTests : BunitContext
.Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>()));
Services.AddSingleton(siteRepo);
// PLAN-01 Task 14 — the Health page now shows the metrics-stale badge with
// a tooltip naming the configured MetricsStaleTimeout, so it injects the
// health-monitoring options.
Services.AddSingleton(Options.Create(new HealthMonitoringOptions()));
// Audit Log (#23) M7 Bundle E — the Health page now also fetches the
// Audit KPI snapshot. Stub it with an empty point-in-time reading so
// the existing assertions (Notification Outbox tiles, Online/Offline
@@ -275,6 +280,62 @@ public class HealthPageTests : BunitContext
});
}
[Fact]
public void Renders_MetricsStaleBadge_ForOnlineButStaleSite()
{
// PLAN-01 Task 14: a site that is online (heartbeating) but whose report
// pipeline died shows a distinct "Metrics stale" warning badge.
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = true,
IsMetricsStale = true,
LastHeartbeatAt = DateTimeOffset.UtcNow,
LastReportReceivedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
});
var cut = Render<HealthPage>();
cut.WaitForAssertion(() =>
{
Assert.Contains("Metrics stale", cut.Markup);
// The badge uses the page's Bootstrap warning style.
Assert.Contains("bg-warning", cut.Markup);
});
}
[Fact]
public void Renders_OfflineSince_ForOfflineSiteWithStatusChange()
{
var changedAt = new DateTimeOffset(2026, 7, 8, 12, 34, 56, TimeSpan.Zero);
SeedStates(new SiteHealthState
{
SiteId = "site-a",
IsOnline = false,
LastHeartbeatAt = changedAt,
LastStatusChangeAt = changedAt,
});
var cut = Render<HealthPage>();
cut.WaitForAssertion(() =>
{
Assert.Contains("offline since", cut.Markup);
// The "u" (universal sortable) format the page renders the flip with.
Assert.Contains(changedAt.ToString("u"), cut.Markup);
});
}
// Re-seeds the aggregator substitute with fully-specified states (as opposed
// to SeedSites' minimal online placeholders).
private void SeedStates(params SiteHealthState[] states)
{
var aggregator = Substitute.For<ICentralHealthAggregator>();
aggregator.GetAllSiteStates()
.Returns(states.ToDictionary(s => s.SiteId));
Services.AddSingleton(aggregator);
}
// Re-seeds the aggregator substitute so the trend panel's site selector has
// options. Each site id maps to a minimal online SiteHealthState (a null
// report is fine — the trend panel keys off the site ids, not the report).