From a48a1acd8d155c4d388aabf02299d1593c6b5d49 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 17 May 2026 14:48:44 +0100 Subject: [PATCH] Compute attached-agent problem directly, drop synthesized signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent-offline detail line was being produced by synthesizing a fake FleetGovernanceSignal with key 'agent-attachment' and laundering it through visibleFleetGovernanceSignals so the chip filter would pick it up. With the chip stack gone and the row exposing a single InfrastructureRowProblem, the synthesis is dead weight — it pretended to be server-emitted governance state when it was really derived from the agent's connection state. - Replace agentAttachmentSignal with agentAttachmentProblem, returning InfrastructureRowProblem directly. The row builder applies it ahead of the fleet-highlights problem, so an offline agent always wins over a downstream credential/config drift signal on the same row. - Remove 'agent-attachment' from FleetGovernanceSignalKey now that no signal carries it. - Simplify the setup-summary Live/Needs attention counters to read off the user-visible row state (statusLabel, problem) instead of re-deriving via signal predicates. The previous path could in principle disagree with what the row actually rendered. - Drop rowFleetSignals, rowHasFleetTone, rowHasVisibleFleetTone — all now unused. --- .../Settings/InfrastructureSourceManager.tsx | 38 +++------------- .../Settings/connectionsTableModel.ts | 21 ++++----- .../Settings/useConnectionsLedger.ts | 44 +++++++++---------- 3 files changed, 36 insertions(+), 67 deletions(-) diff --git a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx index c01141b996..98c3831c73 100644 --- a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx @@ -26,7 +26,6 @@ import { connectionAgentVersionPresentation, infrastructureSourcePresentation, surfaceLabel, - type FleetGovernanceSignal, type InfrastructureSystemRow, } from './connectionsTableModel'; import type { DiscoveredServer, DiscoveryScanStatus } from './infrastructureSettingsModel'; @@ -185,24 +184,6 @@ 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); - -const rowHasVisibleFleetTone = ( - row: InfrastructureSystemRow, - predicate: (signal: FleetGovernanceSignal) => boolean, -): boolean => - [...row.fleetHighlights, ...row.members.flatMap((member) => member.fleetHighlights)].some( - predicate, - ); - const agentHostProfileRouteStep = ( row: InfrastructureSystemRow, ): InfrastructureSourcePickerRouteStep | null => { @@ -393,24 +374,19 @@ export const InfrastructureSourceManager: Component - props - .rows() - .filter((row) => - rowHasFleetTone(row, (signal) => signal.key === 'liveness' && signal.tone === 'ok'), - ).length, + () => props.rows().filter((row) => row.statusLabel === 'Active').length, ); const fleetAttentionSystemCount = createMemo( () => props .rows() - .filter((row) => - rowHasVisibleFleetTone( - row, - (signal) => signal.tone === 'warning' || signal.tone === 'critical', - ), - ).length, + .filter((row) => Boolean(row.problem) || row.members.some((member) => member.problem)) + .length, ); const discoveryReadinessLabel = createMemo(() => { if (props.discoveryScanStatus().scanning) return 'Scanning now'; diff --git a/frontend-modern/src/components/Settings/connectionsTableModel.ts b/frontend-modern/src/components/Settings/connectionsTableModel.ts index af4e93820d..3c2f003485 100644 --- a/frontend-modern/src/components/Settings/connectionsTableModel.ts +++ b/frontend-modern/src/components/Settings/connectionsTableModel.ts @@ -246,8 +246,7 @@ export type FleetGovernanceSignalKey = | 'credential-health' | 'updates' | 'remote-control' - | 'command-policy' - | 'agent-attachment'; + | 'command-policy'; export type FleetGovernanceSignalTone = 'ok' | 'info' | 'warning' | 'critical' | 'muted'; @@ -824,19 +823,18 @@ const commandPolicySignal = (state: ConnectionFleetCommandPolicy): FleetGovernan } }; -// Builds a plain-English chip when an attached Pulse Agent isn't reporting, -// to be rendered alongside a primary connection that *is* healthy. Avoids -// the prior pattern of stacking server-side terms ("Adapter degraded", -// liveness "Stale") that didn't tell the user which collection path failed. -export const agentAttachmentSignal = ( +// Builds the row-level problem line when an attached Pulse Agent isn't +// reporting. Derived directly from connection state — not laundered +// through the fleet-signal pipeline — so the row builder owns the user +// story for "API works, agent doesn't" without a synthesized chip hack. +export const agentAttachmentProblem = ( agent: Connection, -): FleetGovernanceSignal | null => { +): InfrastructureRowProblem | undefined => { switch (agent.state) { case 'stale': { const ago = lastActivityTextFromLastSeen(agent.lastSeen); const suffix = ago && ago !== 'No activity yet' && ago !== 'Unknown' ? ` · ${ago}` : ''; return { - key: 'agent-attachment', label: `Agent offline${suffix}`, detail: agent.stateReason || @@ -846,27 +844,24 @@ export const agentAttachmentSignal = ( } case 'unreachable': return { - key: 'agent-attachment', label: 'Agent unreachable', detail: agent.stateReason || 'Pulse cannot currently reach the agent on this host.', tone: 'critical', }; case 'unauthorized': return { - key: 'agent-attachment', label: 'Agent unauthorized', detail: agent.stateReason || 'The Pulse Agent token is being rejected.', tone: 'critical', }; case 'pending': return { - key: 'agent-attachment', label: 'Agent pending first report', detail: agent.stateReason || 'The Pulse Agent has not reported yet.', tone: 'warning', }; default: - return null; + return undefined; } }; diff --git a/frontend-modern/src/components/Settings/useConnectionsLedger.ts b/frontend-modern/src/components/Settings/useConnectionsLedger.ts index c94566d787..57789e036e 100644 --- a/frontend-modern/src/components/Settings/useConnectionsLedger.ts +++ b/frontend-modern/src/components/Settings/useConnectionsLedger.ts @@ -10,7 +10,7 @@ import { type ConnectionType, } from '@/api/connections'; import { - agentAttachmentSignal, + agentAttachmentProblem, connectionAgentEndpointDisplay, connectionAgentIdentitySummary, connectionLastActivityText, @@ -18,7 +18,7 @@ import { lastActivityTextFromLastSeen, primaryRowProblem, surfaceLabel, - type FleetGovernanceSignal, + type InfrastructureRowProblem, type InfrastructureSourceKind, type InfrastructureSystemMemberRow, type InfrastructureSystemRow, @@ -291,13 +291,14 @@ const buildMemberRow = ( const state = member.state; const presentation = STATE_PRESENTATION[state] ?? STATE_PRESENTATION.pending; const lastSeen = member.lastSeen ?? agentConnection?.lastSeen; - const attachmentSignal = agentConnection ? agentAttachmentSignal(agentConnection) : null; - const fleetSignals: FleetGovernanceSignal[] = [ - ...(attachmentSignal ? [attachmentSignal] : []), - ...(agentConnection ? fleetGovernanceSignalsForConnection(agentConnection) : []), - ]; - + const fleetSignals = agentConnection ? fleetGovernanceSignalsForConnection(agentConnection) : []; const fleetHighlights = visibleFleetGovernanceSignals(fleetSignals); + // Attachment problems (agent offline/unreachable/etc.) are derived from + // connection state directly; everything else (credentials, config, rollout) + // still rolls up from the prioritised highlight list. + const problem = + (agentConnection && agentAttachmentProblem(agentConnection)) ?? + primaryRowProblem(fleetHighlights); return { id: member.id, name, @@ -311,7 +312,7 @@ const buildMemberRow = ( lastActivityText: lastActivityTextFromLastSeen(lastSeen), fleetSignals, fleetHighlights, - problem: primaryRowProblem(fleetHighlights), + problem, primary: Boolean(member.primary), agentConnection, }; @@ -373,12 +374,15 @@ const buildRow = ( connection.id === primaryConnection.id || !memberAgentConnectionIds.has(connection.id), ) : componentConnections; - // For non-agent primaries (PVE, PBS, etc.), surface a clearly-named chip - // when an attached agent is not reporting. We avoid double-injecting the - // chip for cluster member agents — those are tracked on the member rows. - const attachmentSignals: FleetGovernanceSignal[] = + const fleetSignals = rowFleetSignalConnections.flatMap((connection) => + fleetGovernanceSignalsForConnection(connection), + ); + // For non-agent primaries (PVE, PBS, etc.), surface the first unhealthy + // attached-agent connection as the row's problem line. Cluster member + // agents are tracked on the member rows, not duplicated here. + const attachmentProblem: InfrastructureRowProblem | undefined = primaryConnection.type === 'agent' - ? [] + ? undefined : rowFleetSignalConnections .filter( (connection) => @@ -386,14 +390,8 @@ const buildRow = ( connection.type === 'agent' && !memberAgentConnectionIds.has(connection.id), ) - .map((connection) => agentAttachmentSignal(connection)) - .filter((signal): signal is FleetGovernanceSignal => Boolean(signal)); - const fleetSignals: FleetGovernanceSignal[] = [ - ...attachmentSignals, - ...rowFleetSignalConnections.flatMap((connection) => - fleetGovernanceSignalsForConnection(connection), - ), - ]; + .map((connection) => agentAttachmentProblem(connection)) + .find((problem): problem is InfrastructureRowProblem => Boolean(problem)); const fleetHighlights = visibleFleetGovernanceSignals(fleetSignals); return { @@ -414,7 +412,7 @@ const buildRow = ( lastErrorMessage, fleetSignals, fleetHighlights, - problem: primaryRowProblem(fleetHighlights), + problem: attachmentProblem ?? primaryRowProblem(fleetHighlights), enabled: primaryConnection.enabled, canEdit: EDITABLE_CONNECTION_TYPES.includes(primaryConnection.type), canPause: primaryConnection.capabilities.supportsPause,