diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 3cef83b2a..aaa11ca7a 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -243,6 +243,10 @@ included grouped observation, with `last_seen` left only as a compatibility alias during rollout, rather than a promise that every grouped source is healthy at that moment. Lifecycle-adjacent consumers must not label it with generic single-source health wording. +That same ledger read now also includes `latest_included_signal_source`, so +lifecycle-adjacent consumers can attribute the freshest grouped observation to +its canonical reporting source instead of inferring it from the broader grouped +system source set. Lifecycle-adjacent workspace copy must also keep the same commercial framing: infrastructure operations may point operators to Pulse Pro for billing, but it must describe that boundary in monitored-system, plan-limit, and license-status diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index f7d9b8bce..ab6641440 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -257,6 +257,10 @@ That same timestamp is now canonically exposed as during rollout. It represents the freshest included grouped observation, not a claim that every grouped source reported successfully at that time, and API consumers must preserve that meaning in their labeling and presentation. +That canonical signal contract now also includes +`latest_included_signal_source`, so consumers can attribute the freshest +included grouped observation to the source that produced it instead of +guessing from the broader grouped `source` field. 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/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 06561cc58..a244fb45a 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -225,6 +225,10 @@ signal timestamp by its real meaning. The canonical API field is now rollout; it represents the freshest included grouped observation, not a guarantee that every grouped source is healthy, so the UI must not present it with single-source `Last Seen` wording. +That same cloud-paid surface should also show the canonical +`latest_included_signal_source` attribution when present, so a customer can +see which grouped source most recently reported instead of reading an +unqualified aggregate timestamp. Frontend billing/admin surfaces must not synthesize `plan_version` from subscription lifecycle state. When a hosted billing record lacks a plan label, the UI must preserve that absence instead of fabricating values like `active` diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index a795aeda3..fd052a84d 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -381,6 +381,10 @@ freshest grouped observation, with `last_seen` left only as a compatibility alias during rollout, rather than a universal health timestamp, so storage- or recovery-adjacent consumers must not present that field with bare single- source `Last Seen` wording that hides grouped stale/offline conditions. +That same dependency now also includes `latest_included_signal_source`, so +storage- or recovery-adjacent consumers can identify which grouped source most +recently reported instead of deriving attribution from the broader grouped +source field. That same shared `internal/api/` dependency now also assumes self-hosted commercial counting is canonical at the top-level monitored-system boundary: shared setup, deploy, entitlement, and API-backed monitoring helpers may not diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index df18fd722..8c1690e9d 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -184,6 +184,11 @@ grouped source reported more recently than the degraded one, so consumers do not present a fresh `Last Seen` timestamp beside warning or offline state without the canonical explanation of which grouped source is still reporting and which one drifted stale or disconnected. +That same monitored-system contract now also owns attribution for the freshest +included grouped signal. Unified resources must expose not just the timestamp +of that latest included observation but also the canonical source that +produced it, so consumers can say which grouped source most recently reported +instead of showing an unowned aggregate timestamp. The unified-resource runtime now also owns the durable change timeline for the canonical resource view. `internal/unifiedresources/monitor_adapter.go` feeds diff --git a/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts b/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts index 9c4398582..7cab73408 100644 --- a/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts +++ b/frontend-modern/src/api/__tests__/monitoredSystemLedger.test.ts @@ -41,6 +41,7 @@ describe('MonitoredSystemLedgerAPI', () => { reasons: [], }, latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', explanation: { @@ -103,6 +104,7 @@ describe('MonitoredSystemLedgerAPI', () => { type: 'host', status: 'warning', latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', }, @@ -124,6 +126,7 @@ describe('MonitoredSystemLedgerAPI', () => { type: 'host', status: 'warning', latest_included_signal_at: '2026-03-23T11:59:50Z', + latest_included_signal_source: 'docker', last_seen: '2026-03-23T11:59:50Z', source: 'multiple', }, @@ -135,6 +138,7 @@ describe('MonitoredSystemLedgerAPI', () => { const result = await MonitoredSystemLedgerAPI.getLedger(); expect(result.systems[0]?.latest_included_signal_at).toBe('2026-03-23T11:59:50Z'); + expect(result.systems[0]?.latest_included_signal_source).toBe('docker'); }); it('falls back to the deprecated last_seen alias for older payloads', async () => { @@ -155,6 +159,7 @@ describe('MonitoredSystemLedgerAPI', () => { const result = await MonitoredSystemLedgerAPI.getLedger(); expect(result.systems[0]?.latest_included_signal_at).toBe('2026-03-23T11:59:50Z'); + expect(result.systems[0]?.latest_included_signal_source).toBeUndefined(); }); it('preserves canonical status explanation reasons from the API contract', async () => { @@ -179,6 +184,7 @@ describe('MonitoredSystemLedgerAPI', () => { ], }, latest_included_signal_at: '2026-03-23T11:59:50Z', + latest_included_signal_source: 'docker', last_seen: '2026-03-23T11:59:50Z', source: 'multiple', }, @@ -210,6 +216,7 @@ describe('MonitoredSystemLedgerAPI', () => { type: 'host', status: 'degraded', latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', }, diff --git a/frontend-modern/src/api/monitoredSystemLedger.ts b/frontend-modern/src/api/monitoredSystemLedger.ts index 150f9c1af..03ae4d15e 100644 --- a/frontend-modern/src/api/monitoredSystemLedger.ts +++ b/frontend-modern/src/api/monitoredSystemLedger.ts @@ -47,6 +47,7 @@ export interface MonitoredSystemLedgerEntry { status: MonitoredSystemLedgerStatus; status_explanation?: MonitoredSystemLedgerStatusExplanation; latest_included_signal_at: string; // freshest included observation, RFC3339 or empty + latest_included_signal_source?: string; last_seen?: string; // deprecated compatibility alias source: string; explanation?: MonitoredSystemLedgerExplanation; @@ -80,6 +81,9 @@ function normalizeMonitoredSystemLedgerEntry( status, latest_included_signal_at: entry.latest_included_signal_at?.trim() || entry.last_seen?.trim() || '', + latest_included_signal_source: normalizeMonitoredSystemLedgerSource( + entry.latest_included_signal_source ?? (entry.source !== 'multiple' ? entry.source : ''), + ), status_explanation: { summary: entry.status_explanation?.summary ?? defaultMonitoredSystemStatusExplanation(status), reasons: (entry.status_explanation?.reasons ?? []).map(normalizeMonitoredSystemLedgerStatusReason), @@ -144,3 +148,20 @@ function normalizeMonitoredSystemLedgerStatusReasonStatus( return 'unknown'; } } + +function normalizeMonitoredSystemLedgerSource( + source: string | null | undefined, +): string | undefined { + switch ((source ?? '').trim().toLowerCase()) { + case 'agent': + case 'docker': + case 'kubernetes': + case 'pbs': + case 'pmg': + case 'proxmox': + case 'truenas': + return source?.trim().toLowerCase(); + default: + return undefined; + } +} diff --git a/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx b/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx index 682760ad9..57b25a9db 100644 --- a/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx +++ b/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx @@ -55,6 +55,39 @@ function systemStatusExplanation(system: MonitoredSystemLedgerEntry): MonitoredS }; } +function monitoredSystemSourceLabel(source: string | undefined): string { + switch ((source ?? '').trim().toLowerCase()) { + case 'agent': + return 'Agent'; + case 'docker': + return 'Docker'; + case 'kubernetes': + return 'Kubernetes'; + case 'pbs': + return 'PBS'; + case 'pmg': + return 'PMG'; + case 'proxmox': + return 'Proxmox'; + case 'truenas': + return 'TrueNAS'; + default: + return ''; + } +} + +function latestIncludedSignalLabel(system: MonitoredSystemLedgerEntry): string { + if (!system.latest_included_signal_at) { + return '—'; + } + const relative = formatRelativeTime(system.latest_included_signal_at, { compact: true }); + const source = monitoredSystemSourceLabel(system.latest_included_signal_source); + if (source === '') { + return relative; + } + return `${relative} via ${source}`; +} + export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProps = {}) { const [ledger, { refetch }] = createResource(() => MonitoredSystemLedgerAPI.getLedger()); const [expandedSystemKey, setExpandedSystemKey] = createSignal(null); @@ -224,9 +257,7 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp - {system.latest_included_signal_at - ? formatRelativeTime(system.latest_included_signal_at, { compact: true }) - : '—'} + {latestIncludedSignalLabel(system)} diff --git a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx index abf5a15ac..75e47d1f0 100644 --- a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx @@ -80,6 +80,7 @@ describe('MonitoredSystemLedgerPanel', () => { reasons: [], }, latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', explanation: { @@ -144,6 +145,7 @@ describe('MonitoredSystemLedgerPanel', () => { reasons: [], }, latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', explanation: { @@ -180,6 +182,7 @@ describe('MonitoredSystemLedgerPanel', () => { ], }, latest_included_signal_at: '2026-01-02T00:00:00Z', + latest_included_signal_source: 'pbs', last_seen: '2026-01-02T00:00:00Z', source: 'pbs', explanation: { @@ -211,6 +214,7 @@ describe('MonitoredSystemLedgerPanel', () => { expect(screen.getByText('Monitored System Ledger')).toBeInTheDocument(); expect(screen.getByText('Latest Included Signal')).toBeInTheDocument(); + expect(screen.getByText('2026-01-02T00:00:00Z via PBS')).toBeInTheDocument(); expect( screen.getByText( 'Review the monitored systems currently counted against your Pulse Pro plan limit.', @@ -265,6 +269,7 @@ describe('MonitoredSystemLedgerPanel', () => { reasons: [], }, latest_included_signal_at: '2026-01-01T00:00:00Z', + latest_included_signal_source: 'agent', last_seen: '2026-01-01T00:00:00Z', source: 'agent', }, diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 7e1962cb0..cc638dda5 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -594,9 +594,10 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { }, }, }, - LatestIncludedSignalAt: "2026-03-18T17:30:00Z", - LastSeen: "2026-03-18T17:30:00Z", - Source: "agent", + LatestIncludedSignalAt: "2026-03-18T17:30:00Z", + LatestIncludedSignalSource: "agent", + LastSeen: "2026-03-18T17:30:00Z", + Source: "agent", Explanation: MonitoredSystemLedgerExplanation{ Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.", Reasons: []MonitoredSystemLedgerExplanationReason{ @@ -646,6 +647,7 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { ] }, "latest_included_signal_at":"2026-03-18T17:30:00Z", + "latest_included_signal_source":"agent", "last_seen":"2026-03-18T17:30:00Z", "source":"agent", "explanation":{ diff --git a/internal/api/monitored_system_ledger.go b/internal/api/monitored_system_ledger.go index dcfcdd31a..648acb246 100644 --- a/internal/api/monitored_system_ledger.go +++ b/internal/api/monitored_system_ledger.go @@ -13,14 +13,15 @@ import ( // MonitoredSystemLedgerEntry represents a single counted top-level monitored // system. type MonitoredSystemLedgerEntry struct { - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status"` // "online", "warning", "offline", "unknown" - StatusExplanation MonitoredSystemLedgerStatusExplanation `json:"status_explanation"` - LatestIncludedSignalAt string `json:"latest_included_signal_at"` // freshest included observation, RFC3339 or empty - LastSeen string `json:"last_seen,omitempty"` // deprecated compatibility alias for latest_included_signal_at - Source string `json:"source"` - Explanation MonitoredSystemLedgerExplanation `json:"explanation"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` // "online", "warning", "offline", "unknown" + StatusExplanation MonitoredSystemLedgerStatusExplanation `json:"status_explanation"` + LatestIncludedSignalAt string `json:"latest_included_signal_at"` // freshest included observation, RFC3339 or empty + LatestIncludedSignalSource string `json:"latest_included_signal_source,omitempty"` + LastSeen string `json:"last_seen,omitempty"` // deprecated compatibility alias for latest_included_signal_at + Source string `json:"source"` + Explanation MonitoredSystemLedgerExplanation `json:"explanation"` } type MonitoredSystemLedgerStatusExplanation struct { @@ -119,14 +120,15 @@ func (r *Router) handleMonitoredSystemLedger(w http.ResponseWriter, req *http.Re for _, system := range systems { status := normalizeStatus(string(system.Status)) entries = append(entries, MonitoredSystemLedgerEntry{ - Name: system.Name, - Type: system.Type, - Status: status, - StatusExplanation: monitoredSystemLedgerStatusExplanation(system.StatusExplanation, status), - LatestIncludedSignalAt: formatLastSeen(system.LastSeen), - LastSeen: formatLastSeen(system.LastSeen), - Source: system.Source, - Explanation: monitoredSystemLedgerExplanation(system.Explanation), + Name: system.Name, + Type: system.Type, + Status: status, + StatusExplanation: monitoredSystemLedgerStatusExplanation(system.StatusExplanation, status), + LatestIncludedSignalAt: formatLastSeen(system.LastSeen), + LatestIncludedSignalSource: normalizeMonitoredSystemLedgerSource(system.LatestIncludedSignalSource), + LastSeen: formatLastSeen(system.LastSeen), + Source: system.Source, + Explanation: monitoredSystemLedgerExplanation(system.Explanation), }) } @@ -204,6 +206,15 @@ func normalizeMonitoredSystemLedgerReasonStatus(status string) string { } } +func normalizeMonitoredSystemLedgerSource(source string) string { + switch source { + case "agent", "docker", "kubernetes", "pbs", "pmg", "proxmox", "truenas": + return source + default: + return "" + } +} + func formatLastSeen(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 4f5522207..7fd9be5d0 100644 --- a/internal/api/monitored_system_ledger_test.go +++ b/internal/api/monitored_system_ledger_test.go @@ -19,9 +19,10 @@ func TestMonitoredSystemLedgerEntryTypes(t *testing.T) { Summary: "All included top-level collection paths currently report online status.", Reasons: []MonitoredSystemLedgerStatusReason{}, }, - LatestIncludedSignalAt: "2025-01-01T00:00:00Z", - LastSeen: "2025-01-01T00:00:00Z", - Source: "agent", + LatestIncludedSignalAt: "2025-01-01T00:00:00Z", + LatestIncludedSignalSource: "agent", + LastSeen: "2025-01-01T00:00:00Z", + Source: "agent", Explanation: MonitoredSystemLedgerExplanation{ Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.", Reasons: []MonitoredSystemLedgerExplanationReason{ @@ -52,6 +53,9 @@ func TestMonitoredSystemLedgerEntryTypes(t *testing.T) { if decoded.LatestIncludedSignalAt != "2025-01-01T00:00:00Z" { t.Errorf("latest included signal mismatch: %+v", decoded) } + if decoded.LatestIncludedSignalSource != "agent" { + t.Errorf("latest included signal source mismatch: %+v", decoded) + } if decoded.Source != "agent" { t.Errorf("source mismatch: got %q", decoded.Source) } @@ -194,9 +198,10 @@ func TestHandleMonitoredSystemLedgerHTTP(t *testing.T) { Summary: "All included top-level collection paths currently report online status.", Reasons: []MonitoredSystemLedgerStatusReason{}, }, - LatestIncludedSignalAt: "2025-01-01T00:00:00Z", - LastSeen: "2025-01-01T00:00:00Z", - Source: "agent", + LatestIncludedSignalAt: "2025-01-01T00:00:00Z", + LatestIncludedSignalSource: "agent", + LastSeen: "2025-01-01T00:00:00Z", + Source: "agent", Explanation: MonitoredSystemLedgerExplanation{ Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.", Reasons: []MonitoredSystemLedgerExplanationReason{ @@ -240,6 +245,9 @@ func TestHandleMonitoredSystemLedgerHTTP(t *testing.T) { if decoded.Systems[0].LatestIncludedSignalAt != "2025-01-01T00:00:00Z" { t.Errorf("expected latest included signal timestamp, got %+v", decoded.Systems[0]) } + if decoded.Systems[0].LatestIncludedSignalSource != "agent" { + t.Errorf("expected latest included signal source, got %+v", decoded.Systems[0]) + } if decoded.Systems[0].Explanation.Summary == "" { t.Errorf("expected explanation summary, got %+v", decoded.Systems[0].Explanation) } diff --git a/internal/unifiedresources/monitored_systems.go b/internal/unifiedresources/monitored_systems.go index e79a4fbe0..ed28170b6 100644 --- a/internal/unifiedresources/monitored_systems.go +++ b/internal/unifiedresources/monitored_systems.go @@ -64,13 +64,14 @@ type MonitoredSystemStatusReason struct { // MonitoredSystemRecord describes a counted top-level monitored system after // canonical cross-view deduplication. type MonitoredSystemRecord struct { - Name string - Type string - Status ResourceStatus - StatusExplanation MonitoredSystemStatusExplanation - LastSeen time.Time - Source string - Explanation MonitoredSystemGroupingExplanation + Name string + Type string + Status ResourceStatus + StatusExplanation MonitoredSystemStatusExplanation + LastSeen time.Time + LatestIncludedSignalSource string + Source string + Explanation MonitoredSystemGroupingExplanation } // MonitoredSystemCount returns the number of top-level monitored systems after @@ -173,14 +174,16 @@ func resolveMonitoredSystemTopLevelSystems(rs ReadState) TopLevelSystemResolver func monitoredSystemRecord(group monitoredSystemGroup) MonitoredSystemRecord { resource := preferredMonitoredSystemResource(group.resources) status := monitoredSystemStatus(group.resources) + latestSignal := monitoredSystemLatestObservation(group.resources) record := MonitoredSystemRecord{ - Name: monitoredSystemDisplayName(group.resources, resource), - Type: monitoredSystemType(resource), - Status: status, - StatusExplanation: monitoredSystemStatusExplanation(group.resources, status), - LastSeen: monitoredSystemLastSeen(group.resources), - Source: monitoredSystemSource(group.resources), - Explanation: normalizeMonitoredSystemGroupingExplanation(group.explanation), + Name: monitoredSystemDisplayName(group.resources, resource), + Type: monitoredSystemType(resource), + Status: status, + StatusExplanation: monitoredSystemStatusExplanation(group.resources, status), + LastSeen: latestSignal.LastSeen, + LatestIncludedSignalSource: latestSignal.Source, + Source: monitoredSystemSource(group.resources), + Explanation: normalizeMonitoredSystemGroupingExplanation(group.explanation), } if record.Name == "" { record.Name = "Unnamed system" @@ -198,6 +201,9 @@ func monitoredSystemRecord(group monitoredSystemGroup) MonitoredSystemRecord { if record.Source == "" { record.Source = "unknown" } + if record.LatestIncludedSignalSource == "" && record.Source != "multiple" { + record.LatestIncludedSignalSource = record.Source + } if record.Explanation.Summary == "" { record.Explanation = monitoredSystemStandaloneExplanation(group.resources) } @@ -597,7 +603,7 @@ func monitoredSystemStatusSummary( } } -type monitoredSystemLatestObservation struct { +type monitoredSystemObservation struct { Name string Source string LastSeen time.Time @@ -638,8 +644,23 @@ func monitoredSystemHasReasonStatus(reasons []MonitoredSystemStatusReason, statu return false } -func monitoredSystemLatestOnlineObservation(resources []*Resource) monitoredSystemLatestObservation { - var latest monitoredSystemLatestObservation +func monitoredSystemLatestOnlineObservation(resources []*Resource) monitoredSystemObservation { + return monitoredSystemLatestObservationMatching(resources, func(status string) bool { + return status == "online" + }) +} + +func monitoredSystemLatestObservation(resources []*Resource) monitoredSystemObservation { + return monitoredSystemLatestObservationMatching(resources, func(status string) bool { + return status != "" + }) +} + +func monitoredSystemLatestObservationMatching( + resources []*Resource, + include func(status string) bool, +) monitoredSystemObservation { + var latest monitoredSystemObservation for _, resource := range resources { if resource == nil { continue @@ -660,11 +681,12 @@ func monitoredSystemLatestOnlineObservation(resources []*Resource) monitoredSyst }) for _, source := range sourceKeys { sourceStatus := resource.SourceStatus[source] - if normalizeMonitoredSystemSourceStatus(sourceStatus.Status) != "online" { + normalizedStatus := normalizeMonitoredSystemSourceStatus(sourceStatus.Status) + if !include(normalizedStatus) { continue } - if latest.LastSeen.IsZero() || sourceStatus.LastSeen.After(latest.LastSeen) { - latest = monitoredSystemLatestObservation{ + if monitoredSystemObservationIsLater(latest.LastSeen, latest.Source, sourceStatus.LastSeen, string(source)) { + latest = monitoredSystemObservation{ Name: name, Source: string(source), LastSeen: sourceStatus.LastSeen, @@ -673,11 +695,13 @@ func monitoredSystemLatestOnlineObservation(resources []*Resource) monitoredSyst } } - if normalizeMonitoredSystemSourceStatus(string(resource.Status)) == "online" && - (latest.LastSeen.IsZero() || resource.LastSeen.After(latest.LastSeen)) { - latest = monitoredSystemLatestObservation{ + normalizedStatus := normalizeMonitoredSystemSourceStatus(string(resource.Status)) + primarySource := monitoredSystemPrimarySource(resource) + if include(normalizedStatus) && + monitoredSystemObservationIsLater(latest.LastSeen, latest.Source, resource.LastSeen, primarySource) { + latest = monitoredSystemObservation{ Name: name, - Source: monitoredSystemPrimarySource(resource), + Source: primarySource, LastSeen: resource.LastSeen, } } @@ -685,6 +709,27 @@ func monitoredSystemLatestOnlineObservation(resources []*Resource) monitoredSyst return latest } +func monitoredSystemObservationIsLater( + currentAt time.Time, + currentSource string, + candidateAt time.Time, + candidateSource string, +) bool { + if candidateAt.IsZero() { + return false + } + if currentAt.IsZero() { + return true + } + if candidateAt.After(currentAt) { + return true + } + if candidateAt.Equal(currentAt) && strings.TrimSpace(candidateSource) < strings.TrimSpace(currentSource) { + return true + } + return false +} + func monitoredSystemStatusReasonPriority(reason MonitoredSystemStatusReason) int { switch reason.Status { case "offline": @@ -805,16 +850,7 @@ func monitoredSystemSurfaceStatusReasonSummary( } func monitoredSystemLastSeen(resources []*Resource) time.Time { - var lastSeen time.Time - for _, resource := range resources { - if resource == nil || resource.LastSeen.IsZero() { - continue - } - if lastSeen.IsZero() || resource.LastSeen.After(lastSeen) { - lastSeen = resource.LastSeen - } - } - return lastSeen + return monitoredSystemLatestObservation(resources).LastSeen } func monitoredSystemSource(resources []*Resource) string { diff --git a/internal/unifiedresources/registry_test.go b/internal/unifiedresources/registry_test.go index 44a3b8ed0..663036033 100644 --- a/internal/unifiedresources/registry_test.go +++ b/internal/unifiedresources/registry_test.go @@ -300,6 +300,9 @@ func TestMonitoredSystemsExplainsStaleGroupedSourceWhileLastSeenStaysFresh(t *te if !system.LastSeen.Equal(dockerResource.LastSeen) { t.Fatalf("expected grouped last_seen %s, got %s", dockerResource.LastSeen, system.LastSeen) } + if system.LatestIncludedSignalSource != string(SourceDocker) { + t.Fatalf("expected latest included signal source docker, got %+v", system) + } if system.StatusExplanation.Summary == "" { t.Fatal("expected grouped monitored system status explanation summary") }