Expose monitored system latest signal source

This commit is contained in:
rcourtman 2026-03-23 23:08:50 +00:00
parent 87a3ec8736
commit 148c7e8ab5
14 changed files with 207 additions and 62 deletions

View file

@ -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

View file

@ -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

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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',
},

View file

@ -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;
}
}

View file

@ -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<string | null>(null);
@ -224,9 +257,7 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp
</TableCell>
<TableCell>
<span class="text-xs text-muted">
{system.latest_included_signal_at
? formatRelativeTime(system.latest_included_signal_at, { compact: true })
: '—'}
{latestIncludedSignalLabel(system)}
</span>
</TableCell>
</TableRow>

View file

@ -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',
},

View file

@ -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":{

View file

@ -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 ""

View file

@ -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)
}

View file

@ -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 {

View file

@ -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")
}