Rename monitored system reason timestamp

This commit is contained in:
rcourtman
2026-03-24 11:46:54 +00:00
parent c3902d22d0
commit 2a8c315e20
13 changed files with 199 additions and 91 deletions
@@ -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
@@ -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
@@ -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 ledgers 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`
@@ -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
@@ -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).',
},
]);
@@ -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<MonitoredSystemLedgerResponse, 'sys
systems?: MonitoredSystemLedgerRawEntry[];
};
interface MonitoredSystemLedgerRawStatusExplanation
extends Omit<MonitoredSystemLedgerStatusExplanation, 'reasons'> {
reasons?: MonitoredSystemLedgerRawStatusReason[];
}
interface MonitoredSystemLedgerRawStatusReason
extends Omit<MonitoredSystemLedgerStatusReason, 'reported_at'> {
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,
};
}
@@ -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).',
},
@@ -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.',
);
+8 -8
View File
@@ -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)."
}
]
+16 -16
View File
@@ -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 ""
}
+43 -15
View File
@@ -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) {
+34 -34
View File
@@ -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 {
+2 -2
View File
@@ -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)