diff --git a/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs b/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs new file mode 100644 index 0000000..2c1f769 --- /dev/null +++ b/HealthMonitoring/HealthMonitoring.Tests/Reporting/DashboardModelsTests.cs @@ -0,0 +1,24 @@ +using HealthMonitoring.Domain; +using HealthMonitoring.Reporting; + +namespace HealthMonitoring.Tests.Reporting; + +public sealed class DashboardModelsTests +{ + [Fact] + public void ServiceDashboardRow_preserves_registered_version() + { + var row = new ServiceDashboardRow( + "orders", + "Orders", + "Production", + "2.4.1", + HealthStatus.Healthy, + TimeSpan.FromMilliseconds(20), + 99.9, + DateTimeOffset.UtcNow, + null); + + Assert.Equal("2.4.1", row.Version); + } +} diff --git a/HealthMonitoring/HealthMonitoring/Components/Pages/Overview.razor b/HealthMonitoring/HealthMonitoring/Components/Pages/Overview.razor index 175273d..78fe529 100644 --- a/HealthMonitoring/HealthMonitoring/Components/Pages/Overview.razor +++ b/HealthMonitoring/HealthMonitoring/Components/Pages/Overview.razor @@ -21,7 +21,7 @@ else @foreach (var service in _overview.Services) { -
@service.Name@service.Environment
+
@service.Name@service.EnvironmentVersion: @(string.IsNullOrWhiteSpace(service.Version) ? "not reported" : service.Version)
@(service.ResponseDuration?.TotalMilliseconds.ToString("0") ?? "—") ms
@service.UptimePercent.ToString("0.00")%
diff --git a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor index f9ee400..04437a1 100644 --- a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor +++ b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceDetail.razor @@ -8,7 +8,7 @@ } else { -

SERVICE DETAIL · @_model.Service.Environment

@_model.Service.Name

@(_model.Service.MonitorType == MonitorType.KurrentDb ? "KurrentDB monitor" : _model.Service.HealthUrl.ToString())

Manage dependency links Back to overview
+

SERVICE DETAIL · @_model.Service.Environment

@_model.Service.Name

@(_model.Service.MonitorType == MonitorType.KurrentDb ? "KurrentDB monitor" : _model.Service.HealthUrl.ToString())

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)) { }
diff --git a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceRegister.razor b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceRegister.razor index 6fef353..5faf9a5 100644 --- a/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceRegister.razor +++ b/HealthMonitoring/HealthMonitoring/Components/Pages/ServiceRegister.razor @@ -7,6 +7,7 @@ + @if (_request.MonitorType is MonitorType.KurrentDb or MonitorType.SqlServer) { diff --git a/HealthMonitoring/HealthMonitoring/Reporting/DashboardModels.cs b/HealthMonitoring/HealthMonitoring/Reporting/DashboardModels.cs index 2c226f8..9095be1 100644 --- a/HealthMonitoring/HealthMonitoring/Reporting/DashboardModels.cs +++ b/HealthMonitoring/HealthMonitoring/Reporting/DashboardModels.cs @@ -3,6 +3,6 @@ namespace HealthMonitoring.Reporting; public sealed record DashboardOverview(IReadOnlyList Services, int Healthy, int Degraded, int Unhealthy, int OpenIncidents); -public sealed record ServiceDashboardRow(string ServiceId, string Name, string Environment, HealthStatus Status, TimeSpan? ResponseDuration, double UptimePercent, DateTimeOffset? LastChecked, string? LastError); +public sealed record ServiceDashboardRow(string ServiceId, string Name, string Environment, string? Version, HealthStatus Status, TimeSpan? ResponseDuration, double UptimePercent, DateTimeOffset? LastChecked, string? LastError); public sealed record ResolvedDependencyLink(string DependencyName, string TargetServiceId, string TargetServiceName); public sealed record ServiceDetailModel(MonitoredService Service, ServiceDashboardRow Summary, IReadOnlyList Observations, IReadOnlyList Incidents, IReadOnlyList Checks, IReadOnlyList DependencyLinks); diff --git a/HealthMonitoring/HealthMonitoring/Reporting/DashboardQueryService.cs b/HealthMonitoring/HealthMonitoring/Reporting/DashboardQueryService.cs index 673e7ff..7813278 100644 --- a/HealthMonitoring/HealthMonitoring/Reporting/DashboardQueryService.cs +++ b/HealthMonitoring/HealthMonitoring/Reporting/DashboardQueryService.cs @@ -51,6 +51,6 @@ private static ServiceDashboardRow ToRow(MonitoredService service, ServiceStatus var total = observations.Count; var healthy = observations.Count(observation => observation.Status == HealthStatus.Healthy); var uptime = total == 0 ? 0 : healthy * 100d / total; - return new ServiceDashboardRow(service.ServiceId, service.Name, service.Environment, snapshot?.Status ?? HealthStatus.Unknown, snapshot?.LastResponseDuration, uptime, snapshot?.LastObservedAtUtc, snapshot?.LastError); + return new ServiceDashboardRow(service.ServiceId, service.Name, service.Environment, service.Version, snapshot?.Status ?? HealthStatus.Unknown, snapshot?.LastResponseDuration, uptime, snapshot?.LastObservedAtUtc, snapshot?.LastError); } } diff --git a/HealthMonitoring/HealthMonitoring/wwwroot/app.css b/HealthMonitoring/HealthMonitoring/wwwroot/app.css index 59b5d3e..f6d90a5 100644 --- a/HealthMonitoring/HealthMonitoring/wwwroot/app.css +++ b/HealthMonitoring/HealthMonitoring/wwwroot/app.css @@ -19,5 +19,6 @@ a { color: inherit; text-decoration: none; } .form { max-width: 760px; display: grid; gap: 16px; } label { display: grid; gap: 7px; font-size: 13px; font-weight: 700; } input { border: 1px solid #ccd7dc; border-radius: 7px; padding: 11px; font: inherit; } .form-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } .error { color: #c52f40; } .back-link, .dependency-link, .link-button { color: #2563d8; font-weight: 700; } .back-link { display: inline-block; margin-bottom: 20px; } .mapping-panel { max-width: 1100px; } .mapping-heading, .mapping-row { display: grid; grid-template-columns: 1fr 1fr 180px; gap: 18px; align-items: center; } .mapping-heading { padding: 0 12px 12px; color: #71818a; font-size: 11px; font-weight: 800; text-transform: uppercase; } .mapping-row { padding: 15px 12px; border-top: 1px solid #edf0f1; } .mapping-row a:hover, .dependency-link:hover, .back-link:hover { text-decoration: underline; } .mapping-actions { display: flex; gap: 12px; align-items: center; } .link-button, .danger-button { border: 0; background: none; cursor: pointer; font: inherit; padding: 0; } .danger-button { color: #c52f40; } .mapping-form { border-top: 1px solid #edf0f1; margin-top: 18px; padding-top: 22px; } .mapping-form-grid { display: grid; grid-template-columns: 1fr 1fr auto; gap: 14px; align-items: end; } select { border: 1px solid #ccd7dc; border-radius: 7px; padding: 11px; font: inherit; background: #fff; } .mapping-form-actions { display: flex; gap: 8px; } .mapping-note { background: #f1f6ff; color: #52657b; border-radius: 8px; padding: 13px 15px; margin: 20px 0 0; } @media (max-width: 900px) { .sidebar { width: 180px; } .main-content { padding: 28px 20px; } .stat-grid { grid-template-columns: repeat(2, 1fr); } .table-heading { display: none; } .service-row { grid-template-columns: 2fr 1fr 1fr; } .service-row > :nth-child(4), .service-row > :nth-child(5) { display: none; } } +.service-version, .service-version-detail { color: #52657b; font-size: 12px; } .service-version-detail { margin: 8px 0 0; } .initialise-page { max-width: 1100px; margin: 0 auto; } .initialise-breadcrumb { color: #2563d8; font-size: 14px; margin-bottom: 28px; } .initialise-breadcrumb span { color: #8b9aa3; margin: 0 10px; } .initialise-heading { margin-bottom: 24px; } .initialise-heading h1 { font-size: 40px; margin: 6px 0; } .wizard-steps { display: flex; align-items: center; gap: 12px; margin: 24px auto 28px; max-width: 560px; color: #71818a; } .wizard-step { width: 38px; height: 38px; border-radius: 50%; background: #e7edf1; display: grid; place-items: center; font-weight: 800; color: #52657b; } .wizard-step.active { background: #2563d8; color: #fff; } .wizard-line { height: 1px; background: #ccd7dc; flex: 1; } .initialise-notice { background: #e9f3ff; border: 1px solid #c5defb; color: #1457ad; border-radius: 9px; padding: 15px 18px; display: flex; gap: 12px; align-items: center; margin-bottom: 20px; } .monitor-cards { display: grid; gap: 20px; } .monitor-card { background: #fff; border-radius: 12px; padding: 22px; box-shadow: 0 2px 10px #23323c0b; display: flex; gap: 24px; align-items: flex-start; } .monitor-icon { width: 68px; height: 68px; border-radius: 12px; background: #f0f5fa; display: grid; place-items: center; color: #172d4c; font-size: 35px; flex: 0 0 auto; } .monitor-card-body { flex: 1; } .monitor-card-title { display: flex; align-items: center; gap: 12px; } .monitor-card-title h2 { margin: 0; } .required-badge { background: #dcecff; color: #1457ad; border-radius: 16px; padding: 5px 11px; font-size: 12px; font-weight: 800; } .monitor-state { color: #71818a; margin: 8px 0 18px; } .initialise-footer { display: flex; justify-content: space-between; align-items: center; gap: 20px; border-top: 1px solid #dfe6e9; margin-top: 28px; padding-top: 22px; } .checkbox-label { display: flex; align-items: center; gap: 8px; } .checkbox-label input { accent-color: #2563d8; } .initialise-actions { display: flex; gap: 12px; } .initialise-footnote { color: #71818a; font-size: 13px; margin-top: 20px; } .initialisation-overview { max-width: 900px; margin: 0 auto; } .initialisation-cards { display: grid; gap: 14px; } .initialisation-card { align-items: flex-start; background: #fff; border: 1px solid #e1e8eb; border-radius: 12px; box-shadow: 0 2px 10px #23323c0b; display: flex; gap: 18px; padding: 20px; position: relative; } .initialisation-card.configured { border-color: #39c77c; } .initialisation-card.future { color: #52657b; } .initialisation-icon { align-items: center; background: #eef4f8; border-radius: 10px; color: #2563d8; display: flex; flex: 0 0 auto; font-size: 30px; height: 58px; justify-content: center; width: 58px; } .initialisation-card-body { flex: 1; } .initialisation-card-heading { align-items: center; display: flex; gap: 12px; } .initialisation-card-heading h2 { margin: 0; } .initialisation-card p { color: #71818a; margin: 6px 0 12px; } .initialisation-status { border-radius: 16px; font-size: 12px; font-weight: 800; padding: 5px 11px; } .initialisation-status.complete { background: #dff7e9; color: #15975a; } .initialisation-status.required { background: #ffe4e7; color: #c52f40; } .initialisation-status.soon { background: #eef1f3; color: #71818a; } .initialisation-card-action { color: #2563d8; font-weight: 800; } .initialisation-card-action span, .initialisation-card-arrow { font-size: 20px; margin-left: 7px; } .initialisation-card-arrow { color: #71818a; } .initialisation-footer { align-items: center; border-top: 1px solid #dfe6e9; display: flex; justify-content: space-between; margin-top: 28px; padding-top: 22px; }