From 2a8c315e201e97c1dc3bd79403dd59f6b8a104b9 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 24 Mar 2026 11:46:54 +0000 Subject: [PATCH] Rename monitored system reason timestamp --- .../v6/internal/subsystems/agent-lifecycle.md | 3 +- .../v6/internal/subsystems/api-contracts.md | 8 ++- .../internal/subsystems/storage-recovery.md | 4 +- .../internal/subsystems/unified-resources.md | 15 +++- .../__tests__/monitoredSystemLedger.test.ts | 51 +++++++++++++- .../src/api/monitoredSystemLedger.ts | 25 +++++-- .../MonitoredSystemLedgerPanel.test.tsx | 2 +- .../monitoredSystemModelGuardrails.test.ts | 4 +- internal/api/contract_test.go | 16 ++--- internal/api/monitored_system_ledger.go | 32 ++++----- internal/api/monitored_system_ledger_test.go | 58 ++++++++++++---- .../unifiedresources/monitored_systems.go | 68 +++++++++---------- internal/unifiedresources/registry_test.go | 4 +- 13 files changed, 199 insertions(+), 91 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 6bee3d1ee..6dc6a3be9 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -236,7 +236,8 @@ operators can interpret warning, offline, and unknown states without inventing local status semantics. Those status details are now structured as well: lifecycle-adjacent consumers must preserve the canonical reason list from the ledger read so operators can -see which grouped source or surface degraded and when it last reported, +see which grouped source or surface degraded and its canonical `reported_at` +timestamp, instead of only seeing a generic warning/offline paragraph. That same ledger read also treats the canonical `latest_included_signal` object as the freshest included grouped observation. Lifecycle-adjacent diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 96953b2c5..184d95e7f 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -246,8 +246,9 @@ what online, warning, offline, or unknown means. That nested status explanation is now a structured contract, not summary-only copy: `/api/license/monitored-system-ledger` must preserve the canonical summary plus the ordered reason list from unified resources, including the -degraded source or surface, its status, and its last-seen timestamp, so mixed -fresh/stale grouped systems remain explainable through one governed API shape. +degraded source or surface, its status, and its canonical `reported_at` +timestamp, so mixed fresh/stale grouped systems remain explainable through one +governed API shape. That canonical summary must also carry the mixed-source freshness explanation when the freshest grouped observation came from a different source than the degraded one, so API consumers can show a fresh `Last Seen` value without @@ -259,6 +260,9 @@ The backend payload contract now emits only that structured object, and the frontend monitored-system client should parse that canonical wire contract directly rather than keeping flat alias fallback for `latest_included_signal_at`, `latest_included_signal_source`, or `last_seen`. +The canonical nested status-reason timestamp is `reported_at`; older raw +payloads may map a legacy nested `last_seen` input forward during rollout, but +the normalized client contract must expose only `reported_at`. That client contract must also fail closed when older or partial payloads omit the nested explanation object: the frontend may normalize missing explanation fields to empty reasons/surfaces plus a safe default summary, but it must not diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 349aad43d..aa70fa6a3 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -382,8 +382,8 @@ That same shared `internal/api/` dependency now also assumes monitored-system ledger status details stay canonical and source-aware: storage- or recovery- adjacent consumers may read the ledger’s nested status explanation, but they must preserve the backend-provided reason list for stale or offline grouped -sources instead of reducing those mixed fresh/stale system states back to a -generic label. +sources, including the canonical `reported_at` timestamp, instead of reducing +those mixed fresh/stale system states back to a generic label. That same ledger dependency also treats the canonical `latest_included_signal` object as the freshest grouped observation. Storage- or recovery-adjacent consumers must not present that data with bare single-source `Last Seen` diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 22d75f4cb..67ab5be1d 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -89,6 +89,7 @@ cross-source deduplication. 67. `frontend-modern/src/utils/resourceTypePresentation.ts` 68. `frontend-modern/src/utils/resourceIdentity.ts` 69. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts` +70. `frontend-modern/src/hooks/useDashboardTrends.ts` ## Shared Boundaries @@ -190,9 +191,9 @@ explanations. When a grouped monitored system resolves to warning, offline, or unknown, unified resources must expose the shared summary plus structured degraded-status reasons derived from the grouped top-level resources and their source freshness state, including which source or surface degraded and the -corresponding last-seen timestamp. Billing and support surfaces must consume -that shared reason list instead of trying to infer why a fresh overall -`last_seen` can still coincide with warning status. +canonical degraded-signal `reported_at` timestamp. Billing and support +surfaces must consume that shared reason list instead of trying to infer why a +fresh overall `last_seen` can still coincide with warning status. That same status contract must choose the canonical monitored-system runtime status from the actual grouped top-level resources rather than from an implicit `unknown` baseline. Severity ordering is canonical: `offline` @@ -261,6 +262,14 @@ shape: `InfrastructureSummary.tsx` is the render shell, org-scope lifecycle, and focused-summary state, and `infrastructureSummaryModel.ts` owns chart matching, focused-summary display selection, empty-state wording, and summary-series/metric derivation. +The dashboard overview trend hook now follows that same canonical consumer +contract for infrastructure sparklines: `frontend-modern/src/hooks/useDashboardTrends.ts` +must consume the infrastructure summary chart cache and shared unified-resource +series matching logic instead of issuing bespoke per-resource +`/api/metrics-store/history` fetches for top-CPU and top-memory cards. That +keeps dashboard summary sparklines aligned with canonical resource identity +matching, agent-facet fallback behavior, and first-sample empty-state semantics +already owned by the infrastructure summary surface. The backend AI and Patrol context renderers now derive their canonical change kind, source type, source adapter, actor, reason, and related-resource fragments from `internal/unifiedresources/change_presentation.go`, so the diff --git a/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts b/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts index dcd234fca..b4bef5f1f 100644 --- a/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts +++ b/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts @@ -194,6 +194,55 @@ describe('MonitoredSystemLedgerAPI', () => { }); it('preserves canonical status explanation reasons from the API contract', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + systems: [ + { + name: 'Tower', + type: 'host', + status: 'warning', + status_explanation: { + summary: 'At least one included source is stale, so Pulse marks this monitored system as warning.', + reasons: [ + { + kind: 'source-stale', + name: 'Tower', + type: 'host', + source: 'agent', + status: 'stale', + reported_at: '2026-03-23T11:55:00Z', + summary: 'Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).', + }, + ], + }, + latest_included_signal: { + name: 'tower.local', + type: 'docker-host', + source: 'docker', + at: '2026-03-23T11:59:50Z', + }, + source: 'multiple', + }, + ], + total: 1, + limit: 5, + }); + + const result = await MonitoredSystemLedgerAPI.getLedger(); + + expect(result.systems[0]?.status_explanation?.reasons).toEqual([ + { + kind: 'source-stale', + name: 'Tower', + type: 'host', + source: 'agent', + status: 'stale', + reported_at: '2026-03-23T11:55:00Z', + summary: 'Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).', + }, + ]); + }); + + it('maps legacy status reason last_seen fields onto the canonical reported_at contract', async () => { vi.mocked(apiFetchJSON).mockResolvedValueOnce({ systems: [ { @@ -236,7 +285,7 @@ describe('MonitoredSystemLedgerAPI', () => { type: 'host', source: 'agent', status: 'stale', - last_seen: '2026-03-23T11:55:00Z', + reported_at: '2026-03-23T11:55:00Z', summary: 'Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).', }, ]); diff --git a/frontend-modern/src/api/monitoredSystemLedger.ts b/frontend-modern/src/api/monitoredSystemLedger.ts index ab9741c41..50e51d39c 100644 --- a/frontend-modern/src/api/monitoredSystemLedger.ts +++ b/frontend-modern/src/api/monitoredSystemLedger.ts @@ -41,7 +41,7 @@ export interface MonitoredSystemLedgerStatusReason { type: string; source: string; status: MonitoredSystemLedgerStatusReasonStatus; - last_seen: string; + reported_at: string; summary: string; } @@ -72,7 +72,7 @@ type MonitoredSystemLedgerRawEntry = Omit< MonitoredSystemLedgerEntry, 'status_explanation' | 'latest_included_signal' | 'explanation' > & { - status_explanation?: MonitoredSystemLedgerStatusExplanation; + status_explanation?: MonitoredSystemLedgerRawStatusExplanation; latest_included_signal?: MonitoredSystemLedgerLatestSignal; explanation?: MonitoredSystemLedgerExplanation; }; @@ -81,6 +81,17 @@ type MonitoredSystemLedgerRawResponse = Omit { + reasons?: MonitoredSystemLedgerRawStatusReason[]; +} + +interface MonitoredSystemLedgerRawStatusReason + extends Omit { + reported_at?: string; + last_seen?: string; +} + export class MonitoredSystemLedgerAPI { private static readonly baseUrl = '/api/license/monitored-system-ledger'; @@ -135,12 +146,16 @@ function normalizeMonitoredSystemLedgerStatus( } function normalizeMonitoredSystemLedgerStatusReason( - reason: MonitoredSystemLedgerStatusReason, + reason: MonitoredSystemLedgerRawStatusReason, ): MonitoredSystemLedgerStatusReason { return { - ...reason, + kind: reason.kind, + name: reason.name, + type: reason.type, + source: reason.source, status: normalizeMonitoredSystemLedgerStatusReasonStatus(reason.status), - last_seen: reason.last_seen ?? '', + reported_at: reason.reported_at ?? reason.last_seen ?? '', + summary: reason.summary, }; } diff --git a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx index 355536745..ba8df4028 100644 --- a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx @@ -181,7 +181,7 @@ describe('MonitoredSystemLedgerPanel', () => { type: 'pbs-server', source: 'pbs', status: 'offline', - last_seen: '2026-01-01T23:55:00Z', + reported_at: '2026-01-01T23:55:00Z', summary: 'PBS data for server-b is offline or disconnected (last reported 2026-01-01T23:55:00Z).', }, diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index db55f5c2b..01b3cffe1 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -252,9 +252,11 @@ describe('monitored-system model guardrails', () => { expect(monitoredSystemLedgerApiSource).toContain('getMonitoredSystemExplanationFallbackSummary'); expect(monitoredSystemLedgerApiSource).toContain('type MonitoredSystemLedgerRawEntry ='); expect(monitoredSystemLedgerApiSource).toContain('systems?: MonitoredSystemLedgerRawEntry[];'); + expect(monitoredSystemLedgerApiSource).toContain('reported_at: string;'); + expect(monitoredSystemLedgerApiSource).not.toContain('last_seen: string;'); expect(monitoredSystemLedgerApiSource).not.toContain('latest_included_signal_at?: string;'); expect(monitoredSystemLedgerApiSource).not.toContain('latest_included_signal_source?: string;'); - expect(monitoredSystemLedgerApiSource).not.toContain('last_seen?: string;'); + expect(monitoredSystemLedgerApiSource).toContain('last_seen?: string;'); expect(monitoredSystemLedgerApiSource).not.toContain( 'All included top-level collection paths currently report online status.', ); diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 2f009460d..1575c3f1f 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -584,13 +584,13 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", Reasons: []MonitoredSystemLedgerStatusReason{ { - Kind: "source-stale", - Name: "Tower", - Type: "host", - Source: "agent", - Status: "stale", - LastSeen: "2026-03-18T17:25:00Z", - Summary: "Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z).", + Kind: "source-stale", + Name: "Tower", + Type: "host", + Source: "agent", + Status: "stale", + ReportedAt: "2026-03-18T17:25:00Z", + Summary: "Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z).", }, }, }, @@ -644,7 +644,7 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { "type":"host", "source":"agent", "status":"stale", - "last_seen":"2026-03-18T17:25:00Z", + "reported_at":"2026-03-18T17:25:00Z", "summary":"Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z)." } ] diff --git a/internal/api/monitored_system_ledger.go b/internal/api/monitored_system_ledger.go index 40961ee06..bf491512a 100644 --- a/internal/api/monitored_system_ledger.go +++ b/internal/api/monitored_system_ledger.go @@ -35,13 +35,13 @@ type MonitoredSystemLedgerStatusExplanation struct { } type MonitoredSystemLedgerStatusReason struct { - Kind string `json:"kind"` - Name string `json:"name"` - Type string `json:"type"` - Source string `json:"source"` - Status string `json:"status"` - LastSeen string `json:"last_seen"` - Summary string `json:"summary"` + Kind string `json:"kind"` + Name string `json:"name"` + Type string `json:"type"` + Source string `json:"source"` + Status string `json:"status"` + ReportedAt string `json:"reported_at"` + Summary string `json:"summary"` } type MonitoredSystemLedgerExplanation struct { @@ -177,13 +177,13 @@ func monitoredSystemLedgerStatusExplanation( reasons := make([]MonitoredSystemLedgerStatusReason, 0, len(explanation.Reasons)) for _, reason := range explanation.Reasons { reasons = append(reasons, MonitoredSystemLedgerStatusReason{ - Kind: reason.Kind, - Name: reason.Name, - Type: reason.Type, - Source: reason.Source, - Status: normalizeMonitoredSystemLedgerReasonStatus(reason.Status), - LastSeen: formatLastSeen(reason.LastSeen), - Summary: reason.Summary, + Kind: reason.Kind, + Name: reason.Name, + Type: reason.Type, + Source: reason.Source, + Status: normalizeMonitoredSystemLedgerReasonStatus(reason.Status), + ReportedAt: formatMonitoredSystemTime(reason.ReportedAt), + Summary: reason.Summary, }) } @@ -236,11 +236,11 @@ func monitoredSystemLedgerLatestSignal( Name: signal.Name, Type: signal.Type, Source: normalizeMonitoredSystemLedgerSource(signal.Source), - At: formatLastSeen(signal.At), + At: formatMonitoredSystemTime(signal.At), } } -func formatLastSeen(t time.Time) string { +func formatMonitoredSystemTime(t time.Time) string { if t.IsZero() { return "" } diff --git a/internal/api/monitored_system_ledger_test.go b/internal/api/monitored_system_ledger_test.go index a3c1de071..9006b6974 100644 --- a/internal/api/monitored_system_ledger_test.go +++ b/internal/api/monitored_system_ledger_test.go @@ -85,16 +85,16 @@ func TestNormalizeStatus(t *testing.T) { } } -func TestFormatLastSeen(t *testing.T) { +func TestFormatMonitoredSystemTime(t *testing.T) { zero := time.Time{} - if got := formatLastSeen(zero); got != "" { - t.Errorf("formatLastSeen(zero) = %q, want empty", got) + if got := formatMonitoredSystemTime(zero); got != "" { + t.Errorf("formatMonitoredSystemTime(zero) = %q, want empty", got) } ts := time.Date(2025, 6, 15, 10, 30, 0, 0, time.UTC) - got := formatLastSeen(ts) + got := formatMonitoredSystemTime(ts) if got != "2025-06-15T10:30:00Z" { - t.Errorf("formatLastSeen = %q, want 2025-06-15T10:30:00Z", got) + t.Errorf("formatMonitoredSystemTime = %q, want 2025-06-15T10:30:00Z", got) } } @@ -103,13 +103,13 @@ func TestMonitoredSystemLedgerStatusExplanation(t *testing.T) { Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", Reasons: []unifiedresources.MonitoredSystemStatusReason{ { - Kind: "source-stale", - Name: "Tower", - Type: "host", - Source: "agent", - Status: "stale", - LastSeen: time.Date(2026, 3, 23, 11, 55, 0, 0, time.UTC), - Summary: "Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).", + Kind: "source-stale", + Name: "Tower", + Type: "host", + Source: "agent", + Status: "stale", + ReportedAt: time.Date(2026, 3, 23, 11, 55, 0, 0, time.UTC), + Summary: "Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).", }, }, }, "warning") @@ -122,8 +122,8 @@ func TestMonitoredSystemLedgerStatusExplanation(t *testing.T) { if got.Reasons[0].Status != "stale" { t.Fatalf("expected stale status reason, got %+v", got.Reasons[0]) } - if got.Reasons[0].LastSeen != "2026-03-23T11:55:00Z" { - t.Fatalf("expected formatted reason last_seen, got %+v", got.Reasons[0]) + if got.Reasons[0].ReportedAt != "2026-03-23T11:55:00Z" { + t.Fatalf("expected formatted reason reported_at, got %+v", got.Reasons[0]) } } @@ -134,7 +134,17 @@ func TestMonitoredSystemLedgerEntryDoesNotEmitCompatibilityAliases(t *testing.T) Status: unifiedresources.StatusWarning, StatusExplanation: unifiedresources.MonitoredSystemStatusExplanation{ Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", - Reasons: []unifiedresources.MonitoredSystemStatusReason{}, + Reasons: []unifiedresources.MonitoredSystemStatusReason{ + { + Kind: "source-stale", + Name: "Tower", + Type: "host", + Source: "agent", + Status: "stale", + ReportedAt: time.Date(2026, 3, 23, 11, 55, 0, 0, time.UTC), + Summary: "Agent data for Tower is stale (last reported 2026-03-23T11:55:00Z).", + }, + }, }, LastSeen: time.Date(2026, 3, 23, 12, 5, 0, 0, time.UTC), LatestIncludedSignal: unifiedresources.MonitoredSystemLatestSignal{ @@ -171,6 +181,24 @@ func TestMonitoredSystemLedgerEntryDoesNotEmitCompatibilityAliases(t *testing.T) if _, ok := decoded["last_seen"]; ok { t.Fatalf("expected last_seen to be absent, got %+v", decoded) } + statusExplanation, ok := decoded["status_explanation"].(map[string]any) + if !ok { + t.Fatalf("expected status_explanation object, got %+v", decoded) + } + reasons, ok := statusExplanation["reasons"].([]any) + if !ok || len(reasons) != 1 { + t.Fatalf("expected one status reason, got %+v", statusExplanation) + } + reason, ok := reasons[0].(map[string]any) + if !ok { + t.Fatalf("expected status reason object, got %+v", reasons[0]) + } + if _, ok := reason["last_seen"]; ok { + t.Fatalf("expected nested reason last_seen to be absent, got %+v", reason) + } + if reason["reported_at"] != "2026-03-23T11:55:00Z" { + t.Fatalf("expected nested reason reported_at, got %+v", reason) + } } func TestMonitoredSystemLedgerResponseEmptyState(t *testing.T) { diff --git a/internal/unifiedresources/monitored_systems.go b/internal/unifiedresources/monitored_systems.go index c03d41609..1cee77b8d 100644 --- a/internal/unifiedresources/monitored_systems.go +++ b/internal/unifiedresources/monitored_systems.go @@ -52,13 +52,13 @@ type MonitoredSystemStatusExplanation struct { // MonitoredSystemStatusReason captures one canonical degraded-status signal // that contributed to the monitored-system runtime status. type MonitoredSystemStatusReason struct { - Kind string - Name string - Type string - Source string - Status string - LastSeen time.Time - Summary string + Kind string + Name string + Type string + Source string + Status string + ReportedAt time.Time + Summary string } // MonitoredSystemLatestSignal captures the freshest included grouped signal @@ -512,8 +512,8 @@ func monitoredSystemStatusReasons(resources []*Resource) []MonitoredSystemStatus if reasons[i].Source != reasons[j].Source { return reasons[i].Source < reasons[j].Source } - if !reasons[i].LastSeen.Equal(reasons[j].LastSeen) { - return reasons[i].LastSeen.Before(reasons[j].LastSeen) + if !reasons[i].ReportedAt.Equal(reasons[j].ReportedAt) { + return reasons[i].ReportedAt.Before(reasons[j].ReportedAt) } return reasons[i].Summary < reasons[j].Summary }) @@ -555,13 +555,13 @@ func monitoredSystemResourceStatusReasons(resource *Resource) []MonitoredSystemS continue } reasons = append(reasons, MonitoredSystemStatusReason{ - Kind: "source-" + normalizedStatus, - Name: name, - Type: resourceType, - Source: string(source), - Status: normalizedStatus, - LastSeen: sourceStatus.LastSeen, - Summary: monitoredSystemSourceStatusReasonSummary(name, source, normalizedStatus, sourceStatus.LastSeen), + Kind: "source-" + normalizedStatus, + Name: name, + Type: resourceType, + Source: string(source), + Status: normalizedStatus, + ReportedAt: sourceStatus.LastSeen, + Summary: monitoredSystemSourceStatusReasonSummary(name, source, normalizedStatus, sourceStatus.LastSeen), }) } } @@ -581,13 +581,13 @@ func monitoredSystemResourceStatusReasons(resource *Resource) []MonitoredSystemS } return []MonitoredSystemStatusReason{ { - Kind: "surface-" + normalizedStatus, - Name: name, - Type: resourceType, - Source: source, - Status: normalizedStatus, - LastSeen: resource.LastSeen, - Summary: monitoredSystemSurfaceStatusReasonSummary(name, resourceType, source, normalizedStatus, resource.LastSeen), + Kind: "surface-" + normalizedStatus, + Name: name, + Type: resourceType, + Source: source, + Status: normalizedStatus, + ReportedAt: resource.LastSeen, + Summary: monitoredSystemSurfaceStatusReasonSummary(name, resourceType, source, normalizedStatus, resource.LastSeen), }, } } @@ -650,12 +650,12 @@ func monitoredSystemMixedStateStatusSummary( } degraded := reasons[0] - if degraded.LastSeen.IsZero() { + if degraded.ReportedAt.IsZero() { return "" } latest := monitoredSystemLatestOnlineObservation(resources) - if latest.LastSeen.IsZero() || !latest.LastSeen.After(degraded.LastSeen) { + if latest.LastSeen.IsZero() || !latest.LastSeen.After(degraded.ReportedAt) { return "" } @@ -798,17 +798,17 @@ func monitoredSystemStatusReasonClause(reason MonitoredSystemStatusReason) strin sourceLabel := monitoredSystemStatusSourceLabel(reason.Source) switch reason.Kind { case "source-stale": - return sourceLabel + " data for " + subject + " is stale (last reported " + reason.LastSeen.UTC().Format(time.RFC3339) + ")" + return sourceLabel + " data for " + subject + " is stale (last reported " + reason.ReportedAt.UTC().Format(time.RFC3339) + ")" case "source-offline": - return sourceLabel + " data for " + subject + " is offline or disconnected" + monitoredSystemStatusLastSeenSuffix(reason.LastSeen) + return sourceLabel + " data for " + subject + " is offline or disconnected" + monitoredSystemStatusReportedAtSuffix(reason.ReportedAt) case "source-unknown": - return sourceLabel + " data for " + subject + " does not report a canonical status yet" + monitoredSystemStatusLastSeenSuffix(reason.LastSeen) + return sourceLabel + " data for " + subject + " does not report a canonical status yet" + monitoredSystemStatusReportedAtSuffix(reason.ReportedAt) case "surface-stale": - return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports warning status from " + sourceLabel + monitoredSystemStatusLastSeenSuffix(reason.LastSeen) + return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports warning status from " + sourceLabel + monitoredSystemStatusReportedAtSuffix(reason.ReportedAt) case "surface-offline": - return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports offline status from " + sourceLabel + monitoredSystemStatusLastSeenSuffix(reason.LastSeen) + return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports offline status from " + sourceLabel + monitoredSystemStatusReportedAtSuffix(reason.ReportedAt) case "surface-unknown": - return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports unknown status from " + sourceLabel + monitoredSystemStatusLastSeenSuffix(reason.LastSeen) + return monitoredSystemGroupingTypeLabel(reason.Type) + " view for " + subject + " currently reports unknown status from " + sourceLabel + monitoredSystemStatusReportedAtSuffix(reason.ReportedAt) default: clause := strings.TrimSpace(reason.Summary) clause = strings.TrimSuffix(clause, ".") @@ -816,11 +816,11 @@ func monitoredSystemStatusReasonClause(reason MonitoredSystemStatusReason) strin } } -func monitoredSystemStatusLastSeenSuffix(lastSeen time.Time) string { - if lastSeen.IsZero() { +func monitoredSystemStatusReportedAtSuffix(reportedAt time.Time) string { + if reportedAt.IsZero() { return "" } - return " (last reported " + lastSeen.UTC().Format(time.RFC3339) + ")" + return " (last reported " + reportedAt.UTC().Format(time.RFC3339) + ")" } func monitoredSystemStatusPriority(status ResourceStatus) int { diff --git a/internal/unifiedresources/registry_test.go b/internal/unifiedresources/registry_test.go index 33ab844ad..8e9721044 100644 --- a/internal/unifiedresources/registry_test.go +++ b/internal/unifiedresources/registry_test.go @@ -332,8 +332,8 @@ func TestMonitoredSystemsExplainsStaleGroupedSourceWhileLastSeenStaysFresh(t *te if reason.Status != "stale" { t.Fatalf("expected stale reason status, got %+v", reason) } - if !reason.LastSeen.Equal(agentResource.LastSeen) { - t.Fatalf("expected stale reason last_seen %s, got %s", agentResource.LastSeen, reason.LastSeen) + if !reason.ReportedAt.Equal(agentResource.LastSeen) { + t.Fatalf("expected stale reason reported_at %s, got %s", agentResource.LastSeen, reason.ReportedAt) } if reason.Summary == "" { t.Fatalf("expected stale reason summary, got %+v", reason)