mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Don't surface agent-fleet governance on pull-based API sources
An unreachable PBS connection rendered its problem line as 'Rollout blocked' — Pulse Agent rollout terminology that makes no sense for a pull-based API source that simply can't be reached. The backend echoes rollout/config governance state onto every connection, and because the liveness signal is intentionally filtered out of the highlight list (it duplicates the status badge), primaryRowProblem fell through to the agent rollout signal. Gate the agent-binary / managed-config governance signals (version drift, config drift, staged rollout, binary updates, remote command policy) on the connection actually running a Pulse Agent / collector (agent, docker, kubernetes). Pull-based API sources (pve, pbs, pmg, vmware, truenas, availability) now only carry source-agnostic posture (enrollment, liveness, credential health, adapter), so an unreachable PBS reflects its reachability instead of fictitious rollout state. Verified against real infra: no API source shows rollout/config/version language; the only problem lines are legitimate 'Agent offline' attachment messages. Also reconcile tests with two intentional UI changes from the prior infrastructure-table commits that the pre-commit hook (lint only, no vitest) did not catch: - The row-level install-agent shortcut is now an icon button; name it 'Install agent' so it stays distinct from the group-level 'Install Pulse Agent' action the tests assert against. - The system name's OS/identity and cluster/member descriptors moved into the name tooltip to keep rows single-line, so the workspace tests assert them via title instead of visible text.
This commit is contained in:
parent
ab9a439f2c
commit
4f52ef88da
5 changed files with 107 additions and 18 deletions
|
|
@ -889,7 +889,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
|
|||
type="button"
|
||||
onClick={handleInstallAgentShortcut}
|
||||
class="inline-flex items-center justify-center rounded p-1 text-blue-700 transition-colors hover:bg-blue-50 dark:text-blue-300 dark:hover:bg-blue-950/30"
|
||||
aria-label="Install Pulse Agent on this system"
|
||||
aria-label="Install agent"
|
||||
title="Install Pulse Agent on this system to add node-local telemetry (temperatures, SMART, host identity)."
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -800,7 +800,10 @@ describe('InfrastructureWorkspace', () => {
|
|||
await waitFor(() => expect(screen.getByText('Unraid')).toBeInTheDocument());
|
||||
expect(screen.queryByText('Pulse Agent hosts')).toBeNull();
|
||||
expect(screen.getByText('Tower')).toBeInTheDocument();
|
||||
expect(screen.getByText('Unraid 7.1.0')).toBeInTheDocument();
|
||||
// The OS/identity descriptor now rides in the system name's tooltip so the
|
||||
// table row stays single-line; the agent detail drawer still shows it as
|
||||
// visible text (asserted below).
|
||||
expect(screen.getByTitle('Tower · Unraid 7.1.0')).toBeInTheDocument();
|
||||
expect(screen.getByText('192.168.0.10')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Add Unraid$/i }));
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=unraid', {
|
||||
|
|
@ -996,9 +999,11 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(within(readiness).getByText('Needs attention').nextElementSibling).toHaveTextContent(
|
||||
'0 systems',
|
||||
);
|
||||
expect(screen.getByText('Cluster · 2 nodes')).toBeInTheDocument();
|
||||
// Cluster and member descriptors moved into the system name's tooltip to
|
||||
// keep table rows single-line.
|
||||
expect(screen.getByTitle('homelab · Cluster · 2 nodes')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Fleet OK')).toBeNull();
|
||||
expect(screen.getByText('Primary node')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('delly · Primary node')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Agent').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('delly')).toBeInTheDocument();
|
||||
expect(screen.getByText('minipc')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -213,6 +213,66 @@ describe('visibleFleetGovernanceSignals', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fleetGovernanceSignalsForConnection connection-type gating', () => {
|
||||
it('omits agent-fleet governance signals for pull-based API sources', () => {
|
||||
const signals = fleetGovernanceSignalsForConnection(
|
||||
connectionFixture({
|
||||
id: 'pbs:pbs-docker',
|
||||
type: 'pbs',
|
||||
name: 'pbs-docker',
|
||||
state: 'unreachable',
|
||||
source: 'manual',
|
||||
fleet: {
|
||||
enrollmentState: 'configured',
|
||||
livenessState: 'unreachable',
|
||||
versionDrift: 'unknown',
|
||||
adapterHealth: 'blocked',
|
||||
configRollout: 'unknown',
|
||||
credentialStatus: 'unknown',
|
||||
updateStatus: 'unknown',
|
||||
remoteControl: 'not-applicable',
|
||||
rollout: { status: 'blocked', reason: 'blocked by the current connection state' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
const keys = signals.map((signal) => signal.key);
|
||||
// The backend echoed a blocked rollout, but an API source has no agent, so
|
||||
// none of the agent-binary / managed-config governance signals must surface
|
||||
// (this is what produced the misleading "Rollout blocked" problem line).
|
||||
expect(signals.map((signal) => signal.label)).not.toContain('Rollout blocked');
|
||||
expect(keys).not.toContain('rollout');
|
||||
expect(keys).not.toContain('config-drift');
|
||||
expect(keys).not.toContain('version');
|
||||
expect(keys).not.toContain('updates');
|
||||
expect(keys).not.toContain('command-policy');
|
||||
// Source-agnostic signals still apply to API sources.
|
||||
expect(keys).toContain('enrollment');
|
||||
expect(keys).toContain('liveness');
|
||||
expect(keys).toContain('credential-health');
|
||||
});
|
||||
|
||||
it('keeps agent-fleet governance signals for agent connections', () => {
|
||||
const keys = fleetGovernanceSignalsForConnection(
|
||||
connectionFixture({
|
||||
fleet: {
|
||||
enrollmentState: 'enrolled',
|
||||
livenessState: 'active',
|
||||
versionDrift: 'behind',
|
||||
adapterHealth: 'healthy',
|
||||
configRollout: 'reported',
|
||||
credentialStatus: 'verified',
|
||||
updateStatus: 'update-available',
|
||||
remoteControl: 'disabled',
|
||||
rollout: { status: 'blocked', reason: 'staged rollout halted' },
|
||||
},
|
||||
}),
|
||||
).map((signal) => signal.key);
|
||||
expect(keys).toContain('rollout');
|
||||
expect(keys).toContain('version');
|
||||
expect(keys).toContain('command-policy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectionAgentIdentitySummary', () => {
|
||||
const withAgentIdentity = (
|
||||
agentIdentity: Partial<ConnectionAgentIdentity> | undefined,
|
||||
|
|
|
|||
|
|
@ -490,13 +490,15 @@ describe('useConnectionsLedger', () => {
|
|||
'Rollout pending',
|
||||
'Version behind',
|
||||
]);
|
||||
// Pull-based API sources (PVE/PBS/etc.) have no Pulse Agent, so agent-fleet
|
||||
// governance (rollout/config/version/command-policy) must not surface on
|
||||
// them. Only source-agnostic posture like credential health applies; an
|
||||
// invalid PVE token still reads "Credentials invalid", and a paused PVE
|
||||
// connection surfaces no fleet highlight at all.
|
||||
expect(byID.get('pve:invalid')?.fleetHighlights.map((signal) => signal.label)).toEqual([
|
||||
'Credentials invalid',
|
||||
'Rollout blocked',
|
||||
]);
|
||||
expect(byID.get('pve:paused')?.fleetHighlights.map((signal) => signal.label)).toEqual([
|
||||
'Rollout paused',
|
||||
]);
|
||||
expect(byID.get('pve:paused')?.fleetHighlights.map((signal) => signal.label)).toEqual([]);
|
||||
expect(
|
||||
byID.get('agent:remote-disabled')?.fleetHighlights.map((signal) => signal.label),
|
||||
).toEqual([]);
|
||||
|
|
|
|||
|
|
@ -762,9 +762,7 @@ const commandPolicySignal = (state: ConnectionFleetCommandPolicy): FleetGovernan
|
|||
// 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,
|
||||
): InfrastructureRowProblem | undefined => {
|
||||
export const agentAttachmentProblem = (agent: Connection): InfrastructureRowProblem | undefined => {
|
||||
switch (agent.state) {
|
||||
case 'stale': {
|
||||
const ago = lastActivityTextFromLastSeen(agent.lastSeen);
|
||||
|
|
@ -800,21 +798,45 @@ export const agentAttachmentProblem = (
|
|||
}
|
||||
};
|
||||
|
||||
// Connections that run a Pulse Agent / collector binary. Agent-binary and
|
||||
// managed-config governance (version drift, config drift, staged rollout,
|
||||
// binary updates, remote command policy) only describes these — a pull-based
|
||||
// API source (PVE, PBS, PMG, vSphere, TrueNAS, availability probe) has no
|
||||
// agent, so the backend's echoed rollout/config state must not be surfaced as
|
||||
// the row's problem. Without this gate an unreachable PBS reads "Rollout
|
||||
// blocked" instead of plainly reflecting that it is unreachable.
|
||||
const AGENT_FLEET_CONNECTION_TYPES: ReadonlySet<ConnectionType> = new Set([
|
||||
'agent',
|
||||
'docker',
|
||||
'kubernetes',
|
||||
]);
|
||||
|
||||
const connectionRunsAgentFleet = (connection: Connection): boolean =>
|
||||
AGENT_FLEET_CONNECTION_TYPES.has(connection.type);
|
||||
|
||||
export const fleetGovernanceSignalsForConnection = (
|
||||
connection: Connection,
|
||||
): FleetGovernanceSignal[] => {
|
||||
const fleet = fleetGovernanceForConnection(connection);
|
||||
return [
|
||||
const runsAgentFleet = connectionRunsAgentFleet(connection);
|
||||
const signals: FleetGovernanceSignal[] = [
|
||||
enrollmentSignal(fleet.enrollmentState, connection),
|
||||
livenessSignal(fleet.livenessState),
|
||||
credentialHealthSignal(credentialHealthFromFleet(fleet)),
|
||||
configDriftSignal(configDriftFromFleet(fleet)),
|
||||
rolloutSignal(rolloutFromFleet(fleet)),
|
||||
versionSignal(fleet.versionDrift),
|
||||
updateSignal(fleet.updateStatus),
|
||||
adapterSignal(fleet.adapterHealth),
|
||||
commandPolicySignal(commandPolicyFromFleet(fleet)),
|
||||
];
|
||||
if (runsAgentFleet) {
|
||||
signals.push(
|
||||
configDriftSignal(configDriftFromFleet(fleet)),
|
||||
rolloutSignal(rolloutFromFleet(fleet)),
|
||||
versionSignal(fleet.versionDrift),
|
||||
updateSignal(fleet.updateStatus),
|
||||
);
|
||||
}
|
||||
signals.push(adapterSignal(fleet.adapterHealth));
|
||||
if (runsAgentFleet) {
|
||||
signals.push(commandPolicySignal(commandPolicyFromFleet(fleet)));
|
||||
}
|
||||
return signals;
|
||||
};
|
||||
|
||||
export const visibleFleetGovernanceSignals = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue