Add fleet governance projection

This commit is contained in:
rcourtman 2026-04-25 23:41:38 +01:00
parent 97bf4af36d
commit 9bd67fe2c1
21 changed files with 1259 additions and 103 deletions

View file

@ -4039,6 +4039,112 @@
"kind": "file"
}
]
},
{
"id": "L22",
"name": "Fleet governance and rollout control",
"target_score": 6,
"current_score": 6,
"status": "partial",
"completion": {
"state": "bounded-residual",
"summary": "Fleet governance and rollout control now has a first-class governed floor: /api/connections carries enrollment, liveness, version drift, adapter health, config rollout, credential status, update posture, and remote-control posture as a canonical fleet projection, and Infrastructure systems surfaces those facts as a central fleet-governance strip plus row-level attention signals without paid-surface or monitor-count gating. Deeper desired-vs-applied config drift, staged rollout operations, richer credential rotation state, and command-policy enforcement remain a named post-RC hardening track.",
"tracking": [
{
"kind": "lane-followup",
"id": "fleet-governance-rollout-control-post-rc-hardening"
}
]
},
"blockers": [],
"subsystems": [],
"evidence": [
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/api-contracts.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/frontend-primitives.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/registry.json",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/storage-recovery.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/api/__tests__/connections.test.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/api/connections.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/__tests__/useConnectionsLedger.test.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/connectionsTableModel.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/useConnectionsLedger.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/api/connections_aggregator.go",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/api/connections_aggregator_test.go",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/api/connections_types.go",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/api/contract_test.go",
"kind": "file"
}
]
}
],
"release_gates": [
@ -4746,6 +4852,17 @@
],
"subsystem_ids": []
},
{
"id": "fleet-governance-rollout-control-post-rc-hardening",
"summary": "Track broader fleet-governance rollout control beyond the current fleet projection floor, including desired-versus-applied config drift, staged config rollout state, richer credential rotation/expiry health, agent command-policy enforcement, and runtime proof across remote-control safety states.",
"owner": "project-owner",
"status": "planned",
"recorded_at": "2026-04-25",
"lane_ids": [
"L22"
],
"subsystem_ids": []
},
{
"id": "platform-admission-execution-post-rc-hardening",
"summary": "Track broader platform-admission execution beyond the current first-lab-ready support-floor projection, including live vCenter proof before VMware can move to supported, recovery/control expansion, and future admitted-platform proof across setup, canonical projections, alerts, assistant read, and bounded control.",
@ -4780,54 +4897,8 @@
"subsystem_ids": []
}
],
"coverage_gaps": [
{
"id": "fleet-governance-v1",
"summary": "L16 covers install and registration continuity, but the governed map still underrepresents fleet governance primitives such as enrollment state, version drift, adapter health, config rollout, credential status, and remote control-plane safety.",
"owner": "project-owner",
"status": "planned",
"recorded_at": "2026-03-17",
"lane_ids": [
"L16"
],
"subsystem_ids": [
"agent-lifecycle"
],
"proposed_resolution": "lane-expansion",
"coverage_impact": 10,
"evidence": [
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md",
"kind": "file"
}
]
}
],
"candidate_lanes": [
{
"id": "fleet-governance-rollout-control",
"name": "Fleet governance and rollout control",
"summary": "Expand the current fleet lane from install and lifecycle continuity into governed enrollment, drift, adapter-health, config-rollout, and credential-status control surfaces.",
"status": "planned",
"recorded_at": "2026-03-17",
"target_id": "v6-product-lane-expansion",
"current_lane_ids": [
"L16"
],
"coverage_gap_ids": [
"fleet-governance-v1"
],
"subsystem_ids": [
"agent-lifecycle"
]
}
],
"coverage_gaps": [],
"candidate_lanes": [],
"work_claims": [],
"open_decisions": [],
"source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",

View file

@ -269,6 +269,13 @@ the manifest's support-floor row.
should only raise row-level attention when an attached or standalone agent
has an update available, while the exact installed version belongs in the
add/edit detail surfaces rather than as a permanent top-level table column.
That compact fleet-control presentation is now backed by the canonical
`/api/connections` `fleet` projection rather than page-local inference:
`connectionsTableModel.ts`, `useConnectionsLedger.ts`, and
`InfrastructureSourceManager.tsx` may format enrollment, liveness, version
drift, adapter health, config rollout, credential status, update posture,
and remote-control posture, but they must not reconstruct those facts from
table copy, status badge labels, or provider-local error strings.
Standalone host rows must still be recognizable at a glance, but that
identity belongs in the existing row cells rather than new diagnostics
columns: the landing table reuses the `System` and `Endpoint` cells for a

View file

@ -1615,6 +1615,13 @@ the infrastructure ledger: `useConnectionsLedger.ts` must derive one canonical
subtitle (`via platform API`, `via Pulse Agent`, or `via platform API and
Pulse Agent`) from the shared system/component payload instead of letting page-
local tables invent their own API-versus-agent badge heuristics.
That same `/api/connections` row contract now also owns the fleet-governance
projection consumed by the infrastructure workspace. `Connection.fleet` is the
canonical machine-readable source for enrollment state, liveness, version
drift, adapter health, config rollout, credential status, update posture, and
remote-control posture; frontend settings surfaces may format those facts, but
must not infer a second fleet state from row labels, error-message text, or
provider-local table heuristics.
That same shared infrastructure-settings boundary also owns install-profile
semantics surfaced by
`frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`:

View file

@ -286,6 +286,10 @@ work extends shared components instead of creating new local variants.
flows open as
secondary interactions from that same destination instead of taking over the
whole page.
The same source-manager workspace may show a compact fleet-governance strip
and row-level fleet attention badges, but those badges must be presentation
of the canonical `/api/connections` `fleet` object rather than another
frontend-owned lifecycle classifier.
Those secondary views must stay under the same single `Infrastructure`
sidebar destination, but they may open in governed modal/dialog chrome when
that preserves the persistent source-manager page behind them.

View file

@ -672,6 +672,7 @@
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
"frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx",
"frontend-modern/src/components/Settings/infrastructureSettingsModel.ts",
"frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx",
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
"frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx",
@ -1001,6 +1002,7 @@
"frontend-modern/src/components/Settings/discoverySettingsModel.ts",
"frontend-modern/src/components/Settings/InfrastructureDiscoverySettingsDialog.tsx",
"frontend-modern/src/components/Settings/infrastructureSettingsModel.ts",
"frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx",
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
"frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx",

View file

@ -288,11 +288,12 @@ querying, and the operator-facing storage health presentation layer.
backend-authored cluster member nodes, adjacent storage/recovery surfaces
may use that composition for explanatory UI only; they must not promote
those child nodes into a second top-level grouped-system taxonomy or infer
per-node storage ownership from the settings payload. The same shared `/api/connections` payload also owns any
agent-version/update facts carried alongside those grouped rows; adjacent
per-node storage ownership from the settings payload. The same shared
`/api/connections` payload also owns any agent-version/update facts and
fleet-governance posture carried alongside those grouped rows; adjacent
storage or recovery surfaces may reuse that signal for operator context,
but must not fork their own version-comparison semantics or another agent
lifecycle vocabulary.
but must not fork their own version-comparison semantics, credential-state
classifier, or another agent lifecycle vocabulary.
22. Keep backend-native platform actions on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app control for Assistant, storage and recovery may consume the refreshed recovery points afterward, but they must not grow a parallel recovery-local action transport or action-specific payload shape.
23. Keep backend-native platform diagnostics on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app log reads for Assistant, storage and recovery may use those diagnostics during investigation, but they must not grow a parallel recovery-local log transport or diagnostic payload shape.
24. Keep backend-native platform configuration reads on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app config for Assistant, storage and recovery may use that runtime shape during investigation, but they must not grow a parallel recovery-local config transport or provider-shaped configuration payload.

View file

@ -104,6 +104,50 @@ describe('ConnectionsAPI', () => {
});
});
it('list() preserves fleet governance metadata on connection rows', async () => {
const connections: Connection[] = [
{
id: 'agent:mini-pc',
type: 'agent',
name: 'mini-pc',
address: 'mini-pc',
state: 'active',
stateReason: '',
enabled: true,
surfaces: ['host'],
scope: { host: true },
lastSeen: '2026-04-22T20:00:00Z',
lastError: null,
source: 'agent',
fleet: {
enrollmentState: 'enrolled',
livenessState: 'active',
versionDrift: 'behind',
adapterHealth: 'healthy',
configRollout: 'reported',
credentialStatus: 'verified',
updateStatus: 'update-available',
remoteControl: 'enabled',
},
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
},
];
mockedApiFetchJSON.mockResolvedValueOnce({ connections });
const result = await ConnectionsAPI.list();
expect(result.connections[0]?.fleet).toEqual({
enrollmentState: 'enrolled',
livenessState: 'active',
versionDrift: 'behind',
adapterHealth: 'healthy',
configRollout: 'reported',
credentialStatus: 'verified',
updateStatus: 'update-available',
remoteControl: 'enabled',
});
});
it('list() preserves agent identity metadata on agent-backed connections', async () => {
const connections: Connection[] = [
{

View file

@ -21,6 +21,24 @@ export type ConnectionState =
export type ConnectionSource = 'manual' | 'agent' | 'script';
export type ConnectionFleetEnrollmentState = 'configured' | 'enrolled' | 'paused' | 'pending';
export type ConnectionFleetLivenessState = ConnectionState;
export type ConnectionFleetVersionDrift = 'behind' | 'current' | 'unknown' | 'not-applicable';
export type ConnectionFleetAdapterHealth =
| 'blocked'
| 'degraded'
| 'healthy'
| 'paused'
| 'unknown';
export type ConnectionFleetConfigRollout = 'configured' | 'paused' | 'reported' | 'unknown';
export type ConnectionFleetCredentialStatus = 'invalid' | 'paused' | 'unknown' | 'verified';
export type ConnectionFleetUpdateStatus =
| 'current'
| 'not-applicable'
| 'unknown'
| 'update-available';
export type ConnectionFleetRemoteControl = 'disabled' | 'enabled' | 'not-applicable';
export interface ConnectionCapabilities {
supportsPause: boolean;
supportsScope: boolean;
@ -32,6 +50,17 @@ export interface ConnectionError {
at: string;
}
export interface ConnectionFleetGovernance {
enrollmentState: ConnectionFleetEnrollmentState;
livenessState: ConnectionFleetLivenessState;
versionDrift: ConnectionFleetVersionDrift;
adapterHealth: ConnectionFleetAdapterHealth;
configRollout: ConnectionFleetConfigRollout;
credentialStatus: ConnectionFleetCredentialStatus;
updateStatus: ConnectionFleetUpdateStatus;
remoteControl: ConnectionFleetRemoteControl;
}
export interface ConnectionAgentIdentity {
hostname?: string;
platform?: string;
@ -61,6 +90,7 @@ export interface Connection {
agentVersion?: string;
expectedAgentVersion?: string;
agentUpdateAvailable?: boolean;
fleet?: ConnectionFleetGovernance;
capabilities: ConnectionCapabilities;
}

View file

@ -20,8 +20,10 @@ import {
} from '@/components/shared/Table';
import {
connectionAgentVersionPresentation,
fleetSignalClassName,
infrastructureSourcePresentation,
surfaceLabel,
type FleetGovernanceSignal,
type InfrastructureSystemRow,
} from './connectionsTableModel';
import type { DiscoveredServer, DiscoveryScanStatus } from './infrastructureSettingsModel';
@ -160,6 +162,16 @@ const rowHasAgentCoverage = (row: InfrastructureSystemRow): boolean =>
(member) => member.source === 'agent' || member.source === 'both' || member.agentConnection,
);
const rowFleetSignals = (row: InfrastructureSystemRow): FleetGovernanceSignal[] => [
...row.fleetSignals,
...row.members.flatMap((member) => member.fleetSignals),
];
const rowHasFleetTone = (
row: InfrastructureSystemRow,
predicate: (signal: FleetGovernanceSignal) => boolean,
): boolean => rowFleetSignals(row).some(predicate);
export const InfrastructureSourceManager: Component<InfrastructureSourceManagerProps> = (props) => {
let layoutContainerRef: HTMLDivElement | undefined;
const products = createMemo(() => getInfrastructureSourceManagerProducts());
@ -231,6 +243,44 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
const apiOnlySystemCount = createMemo(
() => props.rows().filter((row) => rowHasApiCoverage(row) && !rowHasAgentCoverage(row)).length,
);
const liveFleetSystemCount = createMemo(
() =>
props
.rows()
.filter((row) =>
rowHasFleetTone(row, (signal) => signal.key === 'liveness' && signal.tone === 'ok'),
).length,
);
const fleetAttentionSystemCount = createMemo(
() =>
props
.rows()
.filter((row) =>
rowHasFleetTone(row, (signal) => signal.tone === 'warning' || signal.tone === 'critical'),
).length,
);
const credentialIssueSystemCount = createMemo(
() =>
props
.rows()
.filter((row) =>
rowHasFleetTone(
row,
(signal) => signal.key === 'credentials' && signal.tone === 'critical',
),
).length,
);
const remoteControlEnabledSystemCount = createMemo(
() =>
props
.rows()
.filter((row) =>
rowHasFleetTone(
row,
(signal) => signal.key === 'remote-control' && signal.tone === 'info',
),
).length,
);
const discoveryReadinessLabel = createMemo(() => {
if (props.discoveryScanStatus().scanning) return 'Scanning now';
if (discoveredCandidateCount() > 0) return `${discoveredCandidateCount()} to review`;
@ -461,6 +511,57 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
</dl>
<p class="mt-3 text-xs leading-5 text-muted">{setupConfidenceAction().detail}</p>
<div class="mt-5 border-t border-border-subtle pt-4">
<div class="max-w-3xl">
<h3 class="text-sm font-semibold text-base-content">Fleet governance</h3>
<p class="mt-1 text-sm leading-5 text-muted">
Enrollment, liveness, version drift, adapter health, config state, credentials, update
status, and remote-control posture come from the same governed connections ledger.
</p>
</div>
<dl class="mt-3 grid gap-0 border-y border-border-subtle sm:grid-cols-2 xl:grid-cols-5">
<div class="border-b border-border-subtle px-3 py-3 sm:border-r xl:border-b-0">
<dt class="text-[11px] font-medium uppercase tracking-[0.08em] text-muted">
Managed fleet
</dt>
<dd class="mt-1 text-sm font-semibold text-base-content">
{formatCount(connectedSystemCount(), 'system')}
</dd>
</div>
<div class="border-b border-border-subtle px-3 py-3 xl:border-b-0 xl:border-r">
<dt class="text-[11px] font-medium uppercase tracking-[0.08em] text-muted">Live</dt>
<dd class="mt-1 text-sm font-semibold text-base-content">
{formatCount(liveFleetSystemCount(), 'system')}
</dd>
</div>
<div class="border-b border-border-subtle px-3 py-3 sm:border-b-0 sm:border-r">
<dt class="text-[11px] font-medium uppercase tracking-[0.08em] text-muted">
Needs attention
</dt>
<dd class="mt-1 text-sm font-semibold text-base-content">
{formatCount(fleetAttentionSystemCount(), 'system')}
</dd>
</div>
<div class="border-b border-border-subtle px-3 py-3 sm:border-b-0 xl:border-r">
<dt class="text-[11px] font-medium uppercase tracking-[0.08em] text-muted">
Credential issues
</dt>
<dd class="mt-1 text-sm font-semibold text-base-content">
{formatCount(credentialIssueSystemCount(), 'system')}
</dd>
</div>
<div class="px-3 py-3">
<dt class="text-[11px] font-medium uppercase tracking-[0.08em] text-muted">
Remote control
</dt>
<dd class="mt-1 text-sm font-semibold text-base-content">
{formatCount(remoteControlEnabledSystemCount(), 'system')}
</dd>
</div>
</dl>
</div>
</section>
);
@ -651,6 +752,18 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
{row.lastActivityText}
</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-1">
<For each={row.fleetHighlights}>
{(signal) => (
<span
class={fleetSignalClassName(signal.tone)}
title={signal.detail}
>
{signal.label}
</span>
)}
</For>
</div>
</TableCell>
<Show when={actionColumnVisible()}>
@ -765,6 +878,18 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
{member.lastActivityText}
</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-1">
<For each={member.fleetHighlights}>
{(signal) => (
<span
class={fleetSignalClassName(signal.tone)}
title={signal.detail}
>
{signal.label}
</span>
)}
</For>
</div>
</TableCell>
<Show when={actionColumnVisible()}>
@ -1006,6 +1131,16 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
<span class="text-[12px] text-muted/90">
{member.lastActivityText}
</span>
<For each={member.fleetHighlights}>
{(signal) => (
<span
class={fleetSignalClassName(signal.tone)}
title={signal.detail}
>
{signal.label}
</span>
)}
</For>
</div>
</div>
);
@ -1032,6 +1167,16 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
<span class="text-[12px] text-muted/90">
{row.lastActivityText}
</span>
<For each={row.fleetHighlights}>
{(signal) => (
<span
class={fleetSignalClassName(signal.tone)}
title={signal.detail}
>
{signal.label}
</span>
)}
</For>
</div>
<Show when={!props.readOnly && rowInteractive(row)}>
<button

View file

@ -37,6 +37,8 @@ const row = (overrides: Partial<InfrastructureSystemRow> = {}): InfrastructureSy
agentUpdateCount: 0,
lastActivityText: '5s ago',
lastErrorMessage: undefined,
fleetSignals: [],
fleetHighlights: [],
enabled: connection.enabled,
canEdit: true,
canPause: connection.capabilities.supportsPause,

View file

@ -0,0 +1,116 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it } from 'vitest';
import { InfrastructureSourceManager } from '../InfrastructureSourceManager';
import type { FleetGovernanceSignal, InfrastructureSystemRow } from '../connectionsTableModel';
import type { Connection } from '@/api/connections';
const connectionFixture = (overrides: Partial<Connection> = {}): Connection => ({
id: 'agent:host-1',
type: 'agent',
name: 'host-1',
address: 'host-1',
state: 'active',
stateReason: '',
enabled: true,
surfaces: ['host'],
scope: { host: true },
lastSeen: '2026-04-23T12:00:00Z',
lastError: null,
source: 'agent',
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
...overrides,
});
const signal = (overrides: Partial<FleetGovernanceSignal>): FleetGovernanceSignal => ({
key: 'liveness',
label: 'Fleet OK',
detail: 'No fleet warnings.',
tone: 'ok',
...overrides,
});
const row = (overrides: Partial<InfrastructureSystemRow> = {}): InfrastructureSystemRow => {
const connection = overrides.connection ?? connectionFixture();
const fleetSignals = overrides.fleetSignals ?? [
signal({ key: 'liveness', label: 'Live', tone: 'ok' }),
];
return {
id: connection.id,
ownerType: connection.type,
name: connection.name,
subtitle: 'via Pulse Agent',
source: 'agent',
host: connection.address,
coverageLabels: ['Host telemetry'],
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '5s ago',
fleetSignals,
fleetHighlights: overrides.fleetHighlights ?? [signal({})],
enabled: connection.enabled,
canEdit: false,
canPause: false,
canRemove: true,
isAgent: connection.type === 'agent',
isCluster: false,
attachedConnections: [],
members: [],
connection,
...overrides,
};
};
describe('InfrastructureSourceManager fleet governance', () => {
afterEach(() => cleanup());
it('surfaces governed fleet counts and row-level attention signals', () => {
render(() => (
<InfrastructureSourceManager
rows={() => [
row({
fleetSignals: [
signal({ key: 'liveness', label: 'Live', tone: 'ok' }),
signal({ key: 'remote-control', label: 'Remote control enabled', tone: 'info' }),
],
fleetHighlights: [
signal({ key: 'remote-control', label: 'Remote control enabled', tone: 'info' }),
],
}),
row({
id: 'pve:lab',
ownerType: 'pve',
name: 'lab',
source: 'api',
connection: connectionFixture({
id: 'pve:lab',
type: 'pve',
name: 'lab',
source: 'manual',
capabilities: { supportsPause: true, supportsScope: true, supportsTest: true },
}),
fleetSignals: [
signal({ key: 'liveness', label: 'Unauthorized', tone: 'critical' }),
signal({ key: 'credentials', label: 'Credentials invalid', tone: 'critical' }),
],
fleetHighlights: [
signal({ key: 'credentials', label: 'Credentials invalid', tone: 'critical' }),
],
}),
]}
discoveredNodes={() => []}
discoveryEnabled
discoveryScanStatus={() => ({ scanning: false })}
readOnly
/>
));
expect(screen.getByText('Fleet governance')).toBeInTheDocument();
expect(screen.getByText('Managed fleet')).toBeInTheDocument();
expect(screen.getByText('Needs attention')).toBeInTheDocument();
expect(screen.getByText('Credential issues')).toBeInTheDocument();
expect(screen.getByText('Remote control')).toBeInTheDocument();
expect(screen.getByText('Credentials invalid')).toBeInTheDocument();
expect(screen.getByText('Remote control enabled')).toBeInTheDocument();
});
});

View file

@ -1,7 +1,10 @@
import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Connection } from '@/api/connections';
import type { InfrastructureSystemRow } from '../connectionsTableModel';
import type {
InfrastructureSystemMemberRow,
InfrastructureSystemRow,
} from '../connectionsTableModel';
import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
const routeState = vi.hoisted(() => ({
@ -12,6 +15,20 @@ const connectionState = vi.hoisted(() => ({
connections: [] as Connection[],
rows: null as InfrastructureSystemRow[] | null,
}));
const emptyFleetRow = vi.hoisted(
() =>
({
fleetSignals: [],
fleetHighlights: [],
}) as Pick<InfrastructureSystemRow, 'fleetSignals' | 'fleetHighlights'>,
);
const emptyFleetMember = vi.hoisted(
() =>
({
fleetSignals: [],
fleetHighlights: [],
}) as Pick<InfrastructureSystemMemberRow, 'fleetSignals' | 'fleetHighlights'>,
);
const navigateSpy = vi.hoisted(() => vi.fn());
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
const onboardingMetricsTrackers = vi.hoisted(
@ -119,6 +136,7 @@ vi.mock('../useConnectionsLedger', () => ({
agentUpdateCount: 0,
lastActivityText: '1m ago',
lastErrorMessage: connection.lastError?.message,
...emptyFleetRow,
enabled: connection.enabled,
canEdit: ['pve', 'pbs', 'pmg', 'vmware', 'truenas'].includes(connection.type),
canPause: connection.capabilities.supportsPause,
@ -304,8 +322,10 @@ describe('InfrastructureWorkspace', () => {
expect(within(readiness).getByText('Agent coverage')).toBeInTheDocument();
expect(within(readiness).getByText('Needs agent')).toBeInTheDocument();
expect(within(readiness).getByText('Discovery')).toBeInTheDocument();
expect(within(readiness).getAllByText('1 system')).toHaveLength(3);
expect(within(readiness).getByText('0 systems')).toBeInTheDocument();
expect(within(readiness).getByText('Fleet governance')).toBeInTheDocument();
expect(within(readiness).getByText('Managed fleet')).toBeInTheDocument();
expect(within(readiness).getAllByText('1 system')).toHaveLength(4);
expect(within(readiness).getAllByText('0 systems')).toHaveLength(5);
expect(within(readiness).getByText('Discovery off')).toBeInTheDocument();
expect(within(readiness).getByRole('button', { name: /Install agents/i })).toBeInTheDocument();
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
@ -380,6 +400,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
@ -402,6 +423,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: false,
canPause: false,
@ -424,6 +446,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: false,
canPause: false,
@ -543,6 +566,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '3s ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
@ -562,6 +586,7 @@ describe('InfrastructureWorkspace', () => {
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
lastActivityText: '3s ago',
...emptyFleetMember,
primary: true,
},
],
@ -753,6 +778,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '0s ago',
...emptyFleetRow,
enabled: true,
canEdit: false,
canPause: false,
@ -821,6 +847,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 1,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
@ -888,6 +915,7 @@ describe('InfrastructureWorkspace', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
@ -906,6 +934,7 @@ describe('InfrastructureWorkspace', () => {
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
lastActivityText: '1m ago',
...emptyFleetMember,
primary: true,
agentConnection: dellyAgent,
},
@ -919,6 +948,7 @@ describe('InfrastructureWorkspace', () => {
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
lastActivityText: '1m ago',
...emptyFleetMember,
primary: false,
agentConnection: minipcAgent,
},

View file

@ -1,6 +1,19 @@
import { describe, expect, it } from 'vitest';
import { filterRepresentedDiscoveredServers } from '../infrastructureSettingsModel';
import type { InfrastructureSystemRow } from '../connectionsTableModel';
import type {
InfrastructureSystemMemberRow,
InfrastructureSystemRow,
} from '../connectionsTableModel';
const emptyFleetRow = {
fleetSignals: [],
fleetHighlights: [],
} satisfies Pick<InfrastructureSystemRow, 'fleetSignals' | 'fleetHighlights'>;
const emptyFleetMember = {
fleetSignals: [],
fleetHighlights: [],
} satisfies Pick<InfrastructureSystemMemberRow, 'fleetSignals' | 'fleetHighlights'>;
describe('filterRepresentedDiscoveredServers', () => {
it('removes a discovered platform candidate when the same platform member is already represented by host aliases', () => {
@ -17,6 +30,7 @@ describe('filterRepresentedDiscoveredServers', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '4s ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
@ -36,6 +50,7 @@ describe('filterRepresentedDiscoveredServers', () => {
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
lastActivityText: '4s ago',
...emptyFleetMember,
primary: true,
},
],
@ -87,6 +102,7 @@ describe('filterRepresentedDiscoveredServers', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '4s ago',
...emptyFleetRow,
enabled: true,
canEdit: false,
canPause: false,
@ -145,6 +161,7 @@ describe('filterRepresentedDiscoveredServers', () => {
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '4s ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,

View file

@ -23,6 +23,16 @@ describe('useConnectionsLedger', () => {
lastSeen: '2026-04-23T12:00:00Z',
lastError: null,
source: 'agent',
fleet: {
enrollmentState: 'enrolled',
livenessState: 'active',
versionDrift: 'behind',
adapterHealth: 'healthy',
configRollout: 'reported',
credentialStatus: 'verified',
updateStatus: 'update-available',
remoteControl: 'enabled',
},
agentIdentity: {
hostname: 'tower',
platform: 'unraid',
@ -53,6 +63,11 @@ describe('useConnectionsLedger', () => {
isCluster: false,
coverageLabels: ['Host telemetry'],
});
expect(result.rows()[0].fleetHighlights.map((signal) => signal.label)).toEqual([
'Version behind',
'Update available',
'Remote control enabled',
]);
});
it('renders a Proxmox cluster row from the canonical system metadata', async () => {

View file

@ -1,5 +1,16 @@
import type { Connection } from '@/api/connections';
import type { ConnectionType } from '@/api/connections';
import type {
ConnectionFleetAdapterHealth,
ConnectionFleetConfigRollout,
ConnectionFleetCredentialStatus,
ConnectionFleetEnrollmentState,
ConnectionFleetGovernance,
ConnectionFleetLivenessState,
ConnectionFleetRemoteControl,
ConnectionFleetUpdateStatus,
ConnectionFleetVersionDrift,
} from '@/api/connections';
export const lastActivityTextFromLastSeen = (lastSeen?: string | null): string => {
if (!lastSeen) return 'No activity yet';
@ -195,6 +206,373 @@ export const infrastructureSourcePresentation = (
source: InfrastructureSourceKind,
): InfrastructureSourcePresentation => SOURCE_PRESENTATION[source];
export type FleetGovernanceSignalKey =
| 'enrollment'
| 'liveness'
| 'version'
| 'adapter'
| 'config'
| 'credentials'
| 'updates'
| 'remote-control';
export type FleetGovernanceSignalTone = 'ok' | 'info' | 'warning' | 'critical' | 'muted';
export interface FleetGovernanceSignal {
key: FleetGovernanceSignalKey;
label: string;
detail: string;
tone: FleetGovernanceSignalTone;
}
const DEFAULT_FLEET_GOVERNANCE: ConnectionFleetGovernance = {
enrollmentState: 'pending',
livenessState: 'pending',
versionDrift: 'unknown',
adapterHealth: 'unknown',
configRollout: 'unknown',
credentialStatus: 'unknown',
updateStatus: 'unknown',
remoteControl: 'not-applicable',
};
export const fleetGovernanceForConnection = (connection: Connection): ConnectionFleetGovernance =>
connection.fleet ?? DEFAULT_FLEET_GOVERNANCE;
const fleetSignalClassNameByTone: Record<FleetGovernanceSignalTone, string> = {
ok: 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-200',
info: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-200',
warning:
'border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200',
critical:
'border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-900 dark:bg-rose-950/30 dark:text-rose-200',
muted: 'border-border bg-surface-alt text-muted',
};
export const fleetSignalClassName = (tone: FleetGovernanceSignalTone): string =>
`inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ${fleetSignalClassNameByTone[tone]}`;
const enrollmentSignal = (
state: ConnectionFleetEnrollmentState,
connection: Connection,
): FleetGovernanceSignal => {
switch (state) {
case 'configured':
return {
key: 'enrollment',
label: 'Configured',
detail: 'This source is configured in the infrastructure ledger.',
tone: 'ok',
};
case 'enrolled':
return {
key: 'enrollment',
label: 'Enrolled',
detail: 'This agent has reported into the fleet ledger.',
tone: 'ok',
};
case 'paused':
return {
key: 'enrollment',
label: 'Paused',
detail: 'This source is present but currently paused.',
tone: 'muted',
};
case 'pending':
return {
key: 'enrollment',
label: 'Enrollment pending',
detail:
connection.type === 'agent'
? 'Pulse has not received the first agent report yet.'
: 'Pulse has not confirmed this source yet.',
tone: 'warning',
};
}
};
const livenessSignal = (state: ConnectionFleetLivenessState): FleetGovernanceSignal => {
switch (state) {
case 'active':
return {
key: 'liveness',
label: 'Live',
detail: 'Pulse has recent activity for this source.',
tone: 'ok',
};
case 'paused':
return {
key: 'liveness',
label: 'Paused',
detail: 'Pulse is not polling this source while it is paused.',
tone: 'muted',
};
case 'stale':
return {
key: 'liveness',
label: 'Stale',
detail: 'Pulse has not seen recent activity from this source.',
tone: 'warning',
};
case 'pending':
return {
key: 'liveness',
label: 'Pending',
detail: 'Pulse is waiting for first activity from this source.',
tone: 'warning',
};
case 'unauthorized':
return {
key: 'liveness',
label: 'Unauthorized',
detail: 'The configured credential is being rejected.',
tone: 'critical',
};
case 'unreachable':
return {
key: 'liveness',
label: 'Unreachable',
detail: 'Pulse cannot currently reach this source.',
tone: 'critical',
};
}
};
const versionSignal = (state: ConnectionFleetVersionDrift): FleetGovernanceSignal => {
switch (state) {
case 'behind':
return {
key: 'version',
label: 'Version behind',
detail: 'This agent is behind the current Pulse Agent target.',
tone: 'warning',
};
case 'current':
return {
key: 'version',
label: 'Version current',
detail: 'This agent matches the current Pulse Agent target.',
tone: 'ok',
};
case 'unknown':
return {
key: 'version',
label: 'Version unknown',
detail: 'Pulse does not yet have enough version data for this agent.',
tone: 'muted',
};
case 'not-applicable':
return {
key: 'version',
label: 'No agent version',
detail: 'This source is not governed by the Pulse Agent binary version.',
tone: 'muted',
};
}
};
const adapterSignal = (state: ConnectionFleetAdapterHealth): FleetGovernanceSignal => {
switch (state) {
case 'healthy':
return {
key: 'adapter',
label: 'Adapter healthy',
detail: 'The collection adapter is operating normally.',
tone: 'ok',
};
case 'degraded':
return {
key: 'adapter',
label: 'Adapter degraded',
detail: 'The collection adapter needs attention or first confirmation.',
tone: 'warning',
};
case 'blocked':
return {
key: 'adapter',
label: 'Adapter blocked',
detail: 'The collection adapter cannot complete its current read path.',
tone: 'critical',
};
case 'paused':
return {
key: 'adapter',
label: 'Adapter paused',
detail: 'Collection is paused for this source.',
tone: 'muted',
};
case 'unknown':
return {
key: 'adapter',
label: 'Adapter unknown',
detail: 'Pulse has not classified adapter health yet.',
tone: 'muted',
};
}
};
const configSignal = (state: ConnectionFleetConfigRollout): FleetGovernanceSignal => {
switch (state) {
case 'configured':
return {
key: 'config',
label: 'Config set',
detail: 'This source has configured collection scope.',
tone: 'ok',
};
case 'reported':
return {
key: 'config',
label: 'Config reported',
detail: 'This agent is reporting its applied runtime configuration.',
tone: 'ok',
};
case 'paused':
return {
key: 'config',
label: 'Config paused',
detail: 'Config changes are not active while this source is paused.',
tone: 'muted',
};
case 'unknown':
return {
key: 'config',
label: 'Config unknown',
detail: 'Pulse has not received enough runtime configuration state yet.',
tone: 'warning',
};
}
};
const credentialSignal = (state: ConnectionFleetCredentialStatus): FleetGovernanceSignal => {
switch (state) {
case 'verified':
return {
key: 'credentials',
label: 'Credentials verified',
detail: 'The current credential path is accepted.',
tone: 'ok',
};
case 'invalid':
return {
key: 'credentials',
label: 'Credentials invalid',
detail: 'The current credential path is rejected by the source.',
tone: 'critical',
};
case 'paused':
return {
key: 'credentials',
label: 'Credentials paused',
detail: 'Credential checks are paused with this source.',
tone: 'muted',
};
case 'unknown':
return {
key: 'credentials',
label: 'Credentials unknown',
detail: 'Pulse has not verified this credential path yet.',
tone: 'warning',
};
}
};
const updateSignal = (state: ConnectionFleetUpdateStatus): FleetGovernanceSignal => {
switch (state) {
case 'update-available':
return {
key: 'updates',
label: 'Update available',
detail: 'A newer Pulse Agent binary is available for this system.',
tone: 'warning',
};
case 'current':
return {
key: 'updates',
label: 'Update current',
detail: 'This agent is already on the current target version.',
tone: 'ok',
};
case 'unknown':
return {
key: 'updates',
label: 'Update unknown',
detail: 'Pulse cannot yet compare this agent with the target version.',
tone: 'muted',
};
case 'not-applicable':
return {
key: 'updates',
label: 'No agent update',
detail: 'This source is not updated through the Pulse Agent binary rollout.',
tone: 'muted',
};
}
};
const remoteControlSignal = (state: ConnectionFleetRemoteControl): FleetGovernanceSignal => {
switch (state) {
case 'enabled':
return {
key: 'remote-control',
label: 'Remote control enabled',
detail: 'Pulse command execution is enabled for this agent.',
tone: 'info',
};
case 'disabled':
return {
key: 'remote-control',
label: 'Remote control off',
detail: 'Pulse command execution is disabled for this agent.',
tone: 'muted',
};
case 'not-applicable':
return {
key: 'remote-control',
label: 'No remote control',
detail: 'This source does not use Pulse Agent command execution.',
tone: 'muted',
};
}
};
export const fleetGovernanceSignalsForConnection = (
connection: Connection,
): FleetGovernanceSignal[] => {
const fleet = fleetGovernanceForConnection(connection);
return [
enrollmentSignal(fleet.enrollmentState, connection),
livenessSignal(fleet.livenessState),
versionSignal(fleet.versionDrift),
adapterSignal(fleet.adapterHealth),
configSignal(fleet.configRollout),
credentialSignal(fleet.credentialStatus),
updateSignal(fleet.updateStatus),
remoteControlSignal(fleet.remoteControl),
];
};
export const visibleFleetGovernanceSignals = (
signals: readonly FleetGovernanceSignal[],
): FleetGovernanceSignal[] => {
const attention = signals.filter(
(signal) => signal.tone === 'critical' || signal.tone === 'warning',
);
const control = signals.filter((signal) => signal.tone === 'info');
if (attention.length + control.length > 0) {
return [...attention, ...control].slice(0, 3);
}
return [
{
key: 'liveness',
label: 'Fleet OK',
detail:
'Enrollment, liveness, adapter health, credentials, and rollout state have no current warnings.',
tone: 'ok',
},
];
};
export interface InfrastructureSystemMemberRow {
id: string;
name: string;
@ -206,6 +584,8 @@ export interface InfrastructureSystemMemberRow {
statusLabel: string;
statusClassName: string;
lastActivityText: string;
fleetSignals: FleetGovernanceSignal[];
fleetHighlights: FleetGovernanceSignal[];
primary: boolean;
agentConnection?: Connection;
}
@ -224,6 +604,8 @@ export interface InfrastructureSystemRow {
agentUpdateCount: number;
lastActivityText: string;
lastErrorMessage?: string;
fleetSignals: FleetGovernanceSignal[];
fleetHighlights: FleetGovernanceSignal[];
enabled: boolean;
canEdit: boolean;
canPause: boolean;

View file

@ -11,11 +11,13 @@ import {
connectionAgentEndpointDisplay,
connectionAgentIdentitySummary,
connectionLastActivityText,
fleetGovernanceSignalsForConnection,
lastActivityTextFromLastSeen,
surfaceLabel,
type InfrastructureSourceKind,
type InfrastructureSystemMemberRow,
type InfrastructureSystemRow,
visibleFleetGovernanceSignals,
} from './connectionsTableModel';
export { surfaceLabel };
@ -144,9 +146,7 @@ const moreSevereState = (
return CONNECTION_STATE_SEVERITY[right] > CONNECTION_STATE_SEVERITY[left] ? right : left;
};
const oldestTimestamp = (
values: ReadonlyArray<string | null | undefined>,
): string | undefined => {
const oldestTimestamp = (values: ReadonlyArray<string | null | undefined>): string | undefined => {
let oldestMs: number | undefined;
let oldestRaw: string | undefined;
for (const value of values) {
@ -186,6 +186,7 @@ const buildMemberRow = (
agentConnection && state === agentConnection.state
? agentConnection.lastSeen
: (member.lastSeen ?? agentConnection?.lastSeen);
const fleetSignals = agentConnection ? fleetGovernanceSignalsForConnection(agentConnection) : [];
return {
id: member.id,
@ -198,6 +199,8 @@ const buildMemberRow = (
statusLabel: presentation.label,
statusClassName: presentation.badgeClass,
lastActivityText: lastActivityTextFromLastSeen(lastSeen),
fleetSignals,
fleetHighlights: visibleFleetGovernanceSignals(fleetSignals),
primary: Boolean(member.primary),
agentConnection,
};
@ -244,6 +247,9 @@ const buildRow = (
const identitySubtitle = isStandaloneAgent
? (connectionAgentIdentitySummary(primaryConnection) ?? undefined)
: undefined;
const fleetSignals = componentConnections.flatMap((connection) =>
fleetGovernanceSignalsForConnection(connection),
);
return {
id: primaryConnection.id,
@ -261,6 +267,8 @@ const buildRow = (
? lastActivityTextFromLastSeen(rollup.lastSeen)
: connectionLastActivityText(primaryConnection),
lastErrorMessage,
fleetSignals,
fleetHighlights: visibleFleetGovernanceSignals(fleetSignals),
enabled: primaryConnection.enabled,
canEdit: EDITABLE_CONNECTION_TYPES.includes(primaryConnection.type),
canPause: primaryConnection.capabilities.supportsPause,

View file

@ -25,6 +25,27 @@ const connectionStaleThreshold = 2 * time.Minute
// state-derivation stays the same across types.
var connectionAuthErrorPattern = regexp.MustCompile(`(?i)401|403|unauthori[sz]ed|forbidden|authentication|permission denied|invalid (credentials|token|api key)`)
const (
fleetStateActive = "active"
fleetStateBehind = "behind"
fleetStateBlocked = "blocked"
fleetStateConfigured = "configured"
fleetStateCurrent = "current"
fleetStateDegraded = "degraded"
fleetStateDisabled = "disabled"
fleetStateEnabled = "enabled"
fleetStateEnrolled = "enrolled"
fleetStateHealthy = "healthy"
fleetStateInvalid = "invalid"
fleetStateNotApplicable = "not-applicable"
fleetStatePaused = "paused"
fleetStatePending = "pending"
fleetStateReported = "reported"
fleetStateUnknown = "unknown"
fleetStateUpdateAvailable = "update-available"
fleetStateVerified = "verified"
)
// aggregatorInputs bundles everything the aggregator reads. Separating inputs
// from the handler makes the aggregator unit-testable without spinning up a
// monitor or persistence layer.
@ -93,7 +114,7 @@ func buildPVEConnection(inst config.PVEInstance, health map[string]monitoring.In
}
h := health["pve::"+inst.Name]
state, reason, lastSeen, lastError := deriveConnectionState(enabled, h, now)
return Connection{
return withFleetGovernance(Connection{
ID: "pve:" + inst.Name,
Type: ConnectionTypePVE,
Name: inst.Name,
@ -108,7 +129,7 @@ func buildPVEConnection(inst config.PVEInstance, health map[string]monitoring.In
LastError: lastError,
Source: sourceFromString(inst.Source),
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
})
}
func buildPBSConnection(inst config.PBSInstance, health map[string]monitoring.InstanceHealth, now time.Time) Connection {
@ -124,7 +145,7 @@ func buildPBSConnection(inst config.PBSInstance, health map[string]monitoring.In
}
h := health["pbs::"+inst.Name]
state, reason, lastSeen, lastError := deriveConnectionState(enabled, h, now)
return Connection{
return withFleetGovernance(Connection{
ID: "pbs:" + inst.Name,
Type: ConnectionTypePBS,
Name: inst.Name,
@ -139,7 +160,7 @@ func buildPBSConnection(inst config.PBSInstance, health map[string]monitoring.In
LastError: lastError,
Source: sourceFromString(inst.Source),
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
})
}
func buildPMGConnection(inst config.PMGInstance, health map[string]monitoring.InstanceHealth, now time.Time) Connection {
@ -153,7 +174,7 @@ func buildPMGConnection(inst config.PMGInstance, health map[string]monitoring.In
}
h := health["pmg::"+inst.Name]
state, reason, lastSeen, lastError := deriveConnectionState(enabled, h, now)
return Connection{
return withFleetGovernance(Connection{
ID: "pmg:" + inst.Name,
Type: ConnectionTypePMG,
Name: inst.Name,
@ -168,7 +189,7 @@ func buildPMGConnection(inst config.PMGInstance, health map[string]monitoring.In
LastError: lastError,
Source: ConnectionSourceManual,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
})
}
func buildVMwareConnection(inst config.VMwareVCenterInstance, health map[string]monitoring.InstanceHealth, now time.Time) Connection {
@ -185,7 +206,7 @@ func buildVMwareConnection(inst config.VMwareVCenterInstance, health map[string]
if port == 0 {
port = 443
}
return Connection{
return withFleetGovernance(Connection{
ID: "vmware:" + inst.ID,
Type: ConnectionTypeVMware,
Name: inst.Name,
@ -200,7 +221,7 @@ func buildVMwareConnection(inst config.VMwareVCenterInstance, health map[string]
LastError: lastError,
Source: ConnectionSourceManual,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
})
}
func buildTrueNASConnection(inst config.TrueNASInstance, health map[string]monitoring.InstanceHealth, now time.Time) Connection {
@ -225,7 +246,7 @@ func buildTrueNASConnection(inst config.TrueNASInstance, health map[string]monit
port = 80
}
}
return Connection{
return withFleetGovernance(Connection{
ID: "truenas:" + inst.ID,
Type: ConnectionTypeTrueNAS,
Name: inst.Name,
@ -240,7 +261,7 @@ func buildTrueNASConnection(inst config.TrueNASInstance, health map[string]monit
LastError: lastError,
Source: ConnectionSourceManual,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
})
}
// buildAgentConnection derives a connection row from an agent Host record.
@ -284,7 +305,7 @@ func buildAgentConnection(host models.Host, expectedAgentVersion string, now tim
state = ConnectionStateActive
}
return Connection{
return withFleetGovernance(Connection{
ID: "agent:" + host.ID,
Type: ConnectionTypeAgent,
Name: name,
@ -303,9 +324,117 @@ func buildAgentConnection(host models.Host, expectedAgentVersion string, now tim
ExpectedAgentVersion: expectedAgentVersion,
AgentUpdateAvailable: updateAvailable,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
})
}
func withFleetGovernance(conn Connection) Connection {
conn.Fleet = deriveConnectionFleetGovernance(conn)
return conn
}
func deriveConnectionFleetGovernance(conn Connection) ConnectionFleetGovernance {
return ConnectionFleetGovernance{
EnrollmentState: connectionFleetEnrollmentState(conn),
LivenessState: string(conn.State),
VersionDrift: connectionFleetVersionDrift(conn),
AdapterHealth: connectionFleetAdapterHealth(conn),
ConfigRollout: connectionFleetConfigRollout(conn),
CredentialStatus: connectionFleetCredentialStatus(conn),
UpdateStatus: connectionFleetUpdateStatus(conn),
RemoteControl: connectionFleetRemoteControl(conn),
}
}
func connectionFleetEnrollmentState(conn Connection) string {
if conn.Type == ConnectionTypeAgent {
if conn.LastSeen == nil {
return fleetStatePending
}
return fleetStateEnrolled
}
if !conn.Enabled {
return fleetStatePaused
}
return fleetStateConfigured
}
func connectionFleetVersionDrift(conn Connection) string {
if conn.Type != ConnectionTypeAgent {
return fleetStateNotApplicable
}
if strings.TrimSpace(conn.AgentVersion) == "" || strings.TrimSpace(conn.ExpectedAgentVersion) == "" {
return fleetStateUnknown
}
if conn.AgentUpdateAvailable {
return fleetStateBehind
}
return fleetStateCurrent
}
func connectionFleetAdapterHealth(conn Connection) string {
switch conn.State {
case ConnectionStateActive:
return fleetStateHealthy
case ConnectionStateStale, ConnectionStatePending:
return fleetStateDegraded
case ConnectionStateUnauthorized, ConnectionStateUnreachable:
return fleetStateBlocked
case ConnectionStatePaused:
return fleetStatePaused
default:
return fleetStateUnknown
}
}
func connectionFleetConfigRollout(conn Connection) string {
if !conn.Enabled || conn.State == ConnectionStatePaused {
return fleetStatePaused
}
if conn.Type != ConnectionTypeAgent {
return fleetStateConfigured
}
if conn.LastSeen == nil {
return fleetStateUnknown
}
return fleetStateReported
}
func connectionFleetCredentialStatus(conn Connection) string {
switch conn.State {
case ConnectionStateUnauthorized:
return fleetStateInvalid
case ConnectionStatePending:
return fleetStateUnknown
case ConnectionStatePaused:
return fleetStatePaused
default:
return fleetStateVerified
}
}
func connectionFleetUpdateStatus(conn Connection) string {
if conn.Type != ConnectionTypeAgent {
return fleetStateNotApplicable
}
if conn.AgentUpdateAvailable {
return fleetStateUpdateAvailable
}
if strings.TrimSpace(conn.AgentVersion) == "" || strings.TrimSpace(conn.ExpectedAgentVersion) == "" {
return fleetStateUnknown
}
return fleetStateCurrent
}
func connectionFleetRemoteControl(conn Connection) string {
if conn.Type != ConnectionTypeAgent {
return fleetStateNotApplicable
}
if conn.AgentIdentity != nil && conn.AgentIdentity.CommandsEnabled {
return fleetStateEnabled
}
return fleetStateDisabled
}
func connectionAgentIdentityForHost(host models.Host) *ConnectionAgentIdentity {
identity := &ConnectionAgentIdentity{
Hostname: strings.TrimSpace(host.Hostname),

View file

@ -268,6 +268,117 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) {
}
}
func TestBuildConnections_AgentFleetGovernance(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "current",
Hostname: "current",
LastSeen: now,
AgentVersion: "6.0.2",
CommandsEnabled: true,
},
{
ID: "outdated",
Hostname: "outdated",
LastSeen: now,
AgentVersion: "6.0.0",
},
{
ID: "pending",
Hostname: "pending",
},
},
expectedAgentVersion: "6.0.2",
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
current := byID["agent:current"].Fleet
if current.EnrollmentState != fleetStateEnrolled ||
current.LivenessState != fleetStateActive ||
current.VersionDrift != fleetStateCurrent ||
current.AdapterHealth != fleetStateHealthy ||
current.ConfigRollout != fleetStateReported ||
current.CredentialStatus != fleetStateVerified ||
current.UpdateStatus != fleetStateCurrent ||
current.RemoteControl != fleetStateEnabled {
t.Fatalf("current agent fleet governance = %+v", current)
}
outdated := byID["agent:outdated"].Fleet
if outdated.VersionDrift != fleetStateBehind ||
outdated.UpdateStatus != fleetStateUpdateAvailable ||
outdated.RemoteControl != fleetStateDisabled {
t.Fatalf("outdated agent fleet governance = %+v", outdated)
}
pending := byID["agent:pending"].Fleet
if pending.EnrollmentState != fleetStatePending ||
pending.LivenessState != string(ConnectionStatePending) ||
pending.AdapterHealth != fleetStateDegraded ||
pending.ConfigRollout != fleetStateUnknown ||
pending.CredentialStatus != fleetStateUnknown {
t.Fatalf("pending agent fleet governance = %+v", pending)
}
}
func TestBuildConnections_PlatformFleetGovernance(t *testing.T) {
now := time.Now()
lastSuccess := now.Add(-30 * time.Second)
in := aggregatorInputs{
pveInstances: []config.PVEInstance{
{Name: "healthy", Host: "https://healthy.lan:8006"},
{Name: "bad-token", Host: "https://bad-token.lan:8006"},
{Name: "paused", Host: "https://paused.lan:8006", Disabled: true},
},
instanceHealth: map[string]monitoring.InstanceHealth{
"pve::healthy": healthEntry(&lastSuccess, "", "", "closed"),
"pve::bad-token": healthEntry(&lastSuccess, "403 forbidden", "auth", "closed"),
},
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
healthy := byID["pve:healthy"].Fleet
if healthy.EnrollmentState != fleetStateConfigured ||
healthy.LivenessState != fleetStateActive ||
healthy.VersionDrift != fleetStateNotApplicable ||
healthy.AdapterHealth != fleetStateHealthy ||
healthy.ConfigRollout != fleetStateConfigured ||
healthy.CredentialStatus != fleetStateVerified ||
healthy.UpdateStatus != fleetStateNotApplicable ||
healthy.RemoteControl != fleetStateNotApplicable {
t.Fatalf("healthy platform fleet governance = %+v", healthy)
}
badToken := byID["pve:bad-token"].Fleet
if badToken.AdapterHealth != fleetStateBlocked ||
badToken.CredentialStatus != fleetStateInvalid ||
badToken.LivenessState != string(ConnectionStateUnauthorized) {
t.Fatalf("bad token fleet governance = %+v", badToken)
}
paused := byID["pve:paused"].Fleet
if paused.EnrollmentState != fleetStatePaused ||
paused.AdapterHealth != fleetStatePaused ||
paused.ConfigRollout != fleetStatePaused ||
paused.CredentialStatus != fleetStatePaused {
t.Fatalf("paused platform fleet governance = %+v", paused)
}
}
func TestBuildConnections_VMwareAndTrueNASEnabledFlag(t *testing.T) {
in := aggregatorInputs{
vmwareInstances: []config.VMwareVCenterInstance{{

View file

@ -50,6 +50,20 @@ type ConnectionCapabilities struct {
SupportsTest bool `json:"supportsTest"`
}
// ConnectionFleetGovernance is the canonical fleet-control projection for a
// connection row. Every field is derived from existing connection/runtime
// signals; the connections ledger does not persist a second fleet registry.
type ConnectionFleetGovernance struct {
EnrollmentState string `json:"enrollmentState"`
LivenessState string `json:"livenessState"`
VersionDrift string `json:"versionDrift"`
AdapterHealth string `json:"adapterHealth"`
ConfigRollout string `json:"configRollout"`
CredentialStatus string `json:"credentialStatus"`
UpdateStatus string `json:"updateStatus"`
RemoteControl string `json:"remoteControl"`
}
// ConnectionError is the runtime error shape surfaced on a connection row.
// Mirrors monitoring.ErrorDetail but lives in the api package so the type
// stays stable if the internal monitoring shape evolves.
@ -78,24 +92,25 @@ type ConnectionAgentIdentity struct {
// per-type shapes that today require separate fetches and separate table
// renderers.
type Connection struct {
ID string `json:"id"`
Type ConnectionType `json:"type"`
Name string `json:"name"`
Address string `json:"address"`
HostAliases []string `json:"hostAliases,omitempty"`
State ConnectionState `json:"state"`
StateReason string `json:"stateReason,omitempty"`
Enabled bool `json:"enabled"`
Surfaces []string `json:"surfaces"`
Scope map[string]bool `json:"scope"`
LastSeen *time.Time `json:"lastSeen,omitempty"`
LastError *ConnectionError `json:"lastError,omitempty"`
Source ConnectionSource `json:"source"`
AgentIdentity *ConnectionAgentIdentity `json:"agentIdentity,omitempty"`
AgentVersion string `json:"agentVersion,omitempty"`
ExpectedAgentVersion string `json:"expectedAgentVersion,omitempty"`
AgentUpdateAvailable bool `json:"agentUpdateAvailable,omitempty"`
Capabilities ConnectionCapabilities `json:"capabilities"`
ID string `json:"id"`
Type ConnectionType `json:"type"`
Name string `json:"name"`
Address string `json:"address"`
HostAliases []string `json:"hostAliases,omitempty"`
State ConnectionState `json:"state"`
StateReason string `json:"stateReason,omitempty"`
Enabled bool `json:"enabled"`
Surfaces []string `json:"surfaces"`
Scope map[string]bool `json:"scope"`
LastSeen *time.Time `json:"lastSeen,omitempty"`
LastError *ConnectionError `json:"lastError,omitempty"`
Source ConnectionSource `json:"source"`
AgentIdentity *ConnectionAgentIdentity `json:"agentIdentity,omitempty"`
AgentVersion string `json:"agentVersion,omitempty"`
ExpectedAgentVersion string `json:"expectedAgentVersion,omitempty"`
AgentUpdateAvailable bool `json:"agentUpdateAvailable,omitempty"`
Fleet ConnectionFleetGovernance `json:"fleet"`
Capabilities ConnectionCapabilities `json:"capabilities"`
}
type ConnectionSystemComponentRole string

View file

@ -11652,18 +11652,28 @@ func TestContract_ShippedSecurityDocReferencesStayLocal(t *testing.T) {
// commit.
func TestContract_ConnectionPayloadShapeStaysCanonical(t *testing.T) {
conn := Connection{
ID: "pve-lab",
Type: ConnectionTypePVE,
Name: "lab",
Address: "https://pve.lab:8006",
State: ConnectionStateActive,
StateReason: "",
Enabled: true,
Surfaces: []string{"vms", "containers"},
Scope: map[string]bool{"vms": true, "containers": true},
LastSeen: timePtr(time.Date(2026, 4, 19, 10, 0, 0, 0, time.UTC)),
LastError: nil,
Source: ConnectionSourceManual,
ID: "pve-lab",
Type: ConnectionTypePVE,
Name: "lab",
Address: "https://pve.lab:8006",
State: ConnectionStateActive,
StateReason: "",
Enabled: true,
Surfaces: []string{"vms", "containers"},
Scope: map[string]bool{"vms": true, "containers": true},
LastSeen: timePtr(time.Date(2026, 4, 19, 10, 0, 0, 0, time.UTC)),
LastError: nil,
Source: ConnectionSourceManual,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateConfigured,
LivenessState: fleetStateActive,
VersionDrift: fleetStateNotApplicable,
AdapterHealth: fleetStateHealthy,
ConfigRollout: fleetStateConfigured,
CredentialStatus: fleetStateVerified,
UpdateStatus: fleetStateNotApplicable,
RemoteControl: fleetStateNotApplicable,
},
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
system := ConnectionSystem{
@ -11685,7 +11695,7 @@ func TestContract_ConnectionPayloadShapeStaysCanonical(t *testing.T) {
if err != nil {
t.Fatalf("marshal Connection: %v", err)
}
want := `{"connections":[{"id":"pve-lab","type":"pve","name":"lab","address":"https://pve.lab:8006","state":"active","enabled":true,"surfaces":["vms","containers"],"scope":{"containers":true,"vms":true},"lastSeen":"2026-04-19T10:00:00Z","source":"manual","capabilities":{"supportsPause":true,"supportsScope":true,"supportsTest":true}}],"systems":[{"id":"pve-lab","type":"pve","clusterName":"homelab","components":[{"connectionId":"pve-lab","type":"pve","role":"primary"}]}]}`
want := `{"connections":[{"id":"pve-lab","type":"pve","name":"lab","address":"https://pve.lab:8006","state":"active","enabled":true,"surfaces":["vms","containers"],"scope":{"containers":true,"vms":true},"lastSeen":"2026-04-19T10:00:00Z","source":"manual","fleet":{"enrollmentState":"configured","livenessState":"active","versionDrift":"not-applicable","adapterHealth":"healthy","configRollout":"configured","credentialStatus":"verified","updateStatus":"not-applicable","remoteControl":"not-applicable"},"capabilities":{"supportsPause":true,"supportsScope":true,"supportsTest":true}}],"systems":[{"id":"pve-lab","type":"pve","clusterName":"homelab","components":[{"connectionId":"pve-lab","type":"pve","role":"primary"}]}]}`
assertJSONSnapshot(t, body, want)
}
@ -11763,6 +11773,16 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
AgentVersion: "6.0.0",
ExpectedAgentVersion: "6.0.2",
AgentUpdateAvailable: true,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateEnrolled,
LivenessState: fleetStateActive,
VersionDrift: fleetStateBehind,
AdapterHealth: fleetStateHealthy,
ConfigRollout: fleetStateReported,
CredentialStatus: fleetStateVerified,
UpdateStatus: fleetStateUpdateAvailable,
RemoteControl: fleetStateEnabled,
},
Capabilities: ConnectionCapabilities{
SupportsPause: false,
SupportsScope: false,
@ -11775,7 +11795,7 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
t.Fatalf("marshal agent Connection: %v", err)
}
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled"},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
assertJSONSnapshot(t, body, want)
}

View file

@ -84,7 +84,7 @@ class SubsystemLookupTest(unittest.TestCase):
{"v6-rc-cut", "v6-rc-stabilization", "v6-ga-promotion", "v6-product-lane-expansion"},
)
self.assertEqual(result["scope"]["control_plane_repo"], "pulse")
self.assertEqual(result["status_summary"]["lane_count"], 21)
self.assertEqual(result["status_summary"]["lane_count"], 22)
file_entry = result["files"][0]
matches = {match["subsystem"] for match in file_entry["matches"]}