From 9bd67fe2c1bb658b14bdbfe8dc9c42b9919b3025 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 25 Apr 2026 23:41:38 +0100 Subject: [PATCH] Add fleet governance projection --- docs/release-control/v6/internal/status.json | 167 +++++--- .../v6/internal/subsystems/agent-lifecycle.md | 7 + .../v6/internal/subsystems/api-contracts.md | 7 + .../subsystems/frontend-primitives.md | 4 + .../v6/internal/subsystems/registry.json | 2 + .../internal/subsystems/storage-recovery.md | 9 +- .../src/api/__tests__/connections.test.ts | 44 ++ frontend-modern/src/api/connections.ts | 30 ++ .../Settings/InfrastructureSourceManager.tsx | 145 +++++++ .../__tests__/ConnectionsTable.test.tsx | 2 + .../InfrastructureSourceManager.test.tsx | 116 ++++++ .../InfrastructureWorkspace.test.tsx | 36 +- .../infrastructureSettingsModel.test.ts | 19 +- .../__tests__/useConnectionsLedger.test.ts | 15 + .../Settings/connectionsTableModel.ts | 382 ++++++++++++++++++ .../Settings/useConnectionsLedger.ts | 14 +- internal/api/connections_aggregator.go | 151 ++++++- internal/api/connections_aggregator_test.go | 111 +++++ internal/api/connections_types.go | 51 ++- internal/api/contract_test.go | 48 ++- .../release_control/subsystem_lookup_test.py | 2 +- 21 files changed, 1259 insertions(+), 103 deletions(-) create mode 100644 frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 7fe005b1e..a35627515 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -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", diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 1236c25e5..5f55f8877 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index ff1cf5cbe..c60779ad1 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -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`: diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 0140d9342..8a1b3002f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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. diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 236304b75..fe5d46b15 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -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", diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 85f83000a..9ebfa0f3e 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -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. diff --git a/frontend-modern/src/api/__tests__/connections.test.ts b/frontend-modern/src/api/__tests__/connections.test.ts index ea1721eb6..2f277b049 100644 --- a/frontend-modern/src/api/__tests__/connections.test.ts +++ b/frontend-modern/src/api/__tests__/connections.test.ts @@ -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[] = [ { diff --git a/frontend-modern/src/api/connections.ts b/frontend-modern/src/api/connections.ts index 88c3f8c62..a4d0db6ac 100644 --- a/frontend-modern/src/api/connections.ts +++ b/frontend-modern/src/api/connections.ts @@ -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; } diff --git a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx index 380137cb8..d728975ce 100644 --- a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx @@ -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 = (props) => { let layoutContainerRef: HTMLDivElement | undefined; const products = createMemo(() => getInfrastructureSourceManagerProducts()); @@ -231,6 +243,44 @@ export const InfrastructureSourceManager: Component 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

{setupConfidenceAction().detail}

+ +
+
+

Fleet governance

+

+ Enrollment, liveness, version drift, adapter health, config state, credentials, update + status, and remote-control posture come from the same governed connections ledger. +

+
+ +
+
+
+ Managed fleet +
+
+ {formatCount(connectedSystemCount(), 'system')} +
+
+
+
Live
+
+ {formatCount(liveFleetSystemCount(), 'system')} +
+
+
+
+ Needs attention +
+
+ {formatCount(fleetAttentionSystemCount(), 'system')} +
+
+
+
+ Credential issues +
+
+ {formatCount(credentialIssueSystemCount(), 'system')} +
+
+
+
+ Remote control +
+
+ {formatCount(remoteControlEnabledSystemCount(), 'system')} +
+
+
+
); @@ -651,6 +752,18 @@ export const InfrastructureSourceManager: Component +
+ + {(signal) => ( + + {signal.label} + + )} + +
@@ -765,6 +878,18 @@ export const InfrastructureSourceManager: Component +
+ + {(signal) => ( + + {signal.label} + + )} + +
@@ -1006,6 +1131,16 @@ export const InfrastructureSourceManager: Component {member.lastActivityText} + + {(signal) => ( + + {signal.label} + + )} + ); @@ -1032,6 +1167,16 @@ export const InfrastructureSourceManager: Component {row.lastActivityText} + + {(signal) => ( + + {signal.label} + + )} +