diff --git a/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs b/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs index 2c1f769..afcc66b 100644 --- a/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs +++ b/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs @@ -21,4 +21,32 @@ public void ServiceDashboardRow_preserves_registered_version() Assert.Equal("2.4.1", row.Version); } + + [Fact] + public void TimelineObservationSelector_returns_latest_100_in_display_order() + { + var observations = Enumerable.Range(0, 101) + .Select(index => new HealthObservation + { + ObservedAtUtc = DateTimeOffset.UtcNow.AddMinutes(index), + Status = HealthStatus.Healthy + }) + .ToArray(); + + var selected = TimelineObservationSelector.Select(observations).ToArray(); + + Assert.Equal(100, selected.Length); + Assert.Equal(observations[1].ObservedAtUtc, selected[0].ObservedAtUtc); + Assert.Equal(observations[100].ObservedAtUtc, selected[^1].ObservedAtUtc); + } + + [Fact] + public void TimelineObservationSelector_exposes_maximum_segments_as_read_only_property() + { + var property = typeof(TimelineObservationSelector).GetProperty(nameof(TimelineObservationSelector.MaximumSegments)); + + Assert.NotNull(property); + Assert.False(property!.CanWrite); + Assert.Equal(100, property.GetValue(null)); + } } diff --git a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor index 78213e1..56cd831 100644 --- a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor +++ b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor @@ -11,7 +11,7 @@ else

SERVICE DETAIL · @_model.Service.Environment

@_model.Service.Name

@MonitorDescription

Version: @(string.IsNullOrWhiteSpace(_model.Service.Version) ? "not reported" : _model.Service.Version)

Manage dependency links Back to overview
Last checked @( _model.Summary.LastChecked?.ToLocalTime().ToString("dd MMM, HH:mm:ss") ?? "never")
-

Status timeline

@foreach (var observation in _model.Observations.OrderBy(item => item.ObservedAtUtc)) { }
+

Status timeline

@foreach (var observation in TimelineObservationSelector.Select(_model.Observations)) { }

Dependency details

@if (_model.Checks.Count == 0) {

No dependency details have been returned.

} else {
CheckStatusDurationDetails
@foreach (var check in LatestChecks) { var link = _model.DependencyLinks.FirstOrDefault(item => string.Equals(item.DependencyName.Trim(), check.Result.Name.Trim(), StringComparison.OrdinalIgnoreCase));
@if (link is null) { @check.Result.Name } else { @check.Result.Name ↗ }
@check.Result.Duration.TotalMilliseconds.ToString("0") ms
@(check.Result.Description ?? check.Result.Exception ?? "No detail")
}
}
Latest raw health response
@(_model.Observations.FirstOrDefault()?.RawResponseJson ?? "No response recorded.")
} diff --git a/HealthMonitoring/HealthMonitoring/Reporting/TimelineObservationSelector.cs b/HealthMonitoring/HealthMonitoring/Reporting/TimelineObservationSelector.cs new file mode 100644 index 0000000..0e5b018 --- /dev/null +++ b/HealthMonitoring/HealthMonitoring/Reporting/TimelineObservationSelector.cs @@ -0,0 +1,13 @@ +using HealthMonitoring.Domain; + +namespace HealthMonitoring.Reporting; + +public static class TimelineObservationSelector +{ + public static int MaximumSegments => 100; + + public static IEnumerable Select(IEnumerable observations) => observations + .OrderByDescending(observation => observation.ObservedAtUtc) + .Take(MaximumSegments) + .OrderBy(observation => observation.ObservedAtUtc); +}