fix: align infrastructure source manager actions

This commit is contained in:
rcourtman 2026-04-24 19:02:18 +01:00
parent fb6b53268a
commit f50f0b0136
8 changed files with 87 additions and 104 deletions

View file

@ -248,7 +248,7 @@ an add-only capacity posture.
mismatched, and signature-invalid grants through agent metrics.
9. Add or change profile management, the extracted agent profiles runtime owner, the infrastructure source-manager landing, the pure unified-agent inventory/install model, the connections-ledger workspace shell, the unified ConnectionEditor and its per-type credential slots, route model, shared install section owner, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system admission preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/ConnectionsTable.tsx`, `frontend-modern/src/components/Settings/connectionsTableModel.ts`, `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, `frontend-modern/src/components/Settings/useConnectionRowActions.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. Phase 9 retired the legacy reporting/inventory surface (InfrastructureOperationsController, InfrastructureInventorySection, InfrastructureActiveRowDetails, InfrastructureIgnoredRowDetails, InfrastructureStopMonitoringDialog, useInfrastructureReportingState) and the per-type shells (PlatformConnectionsWorkspace, ProxmoxSettingsPanel, ProxmoxDirectWorkspace, ProxmoxConfiguredNodesTable, ProxmoxDirectConnectionsCard, ProxmoxDiscoveryResultsCard, ProxmoxDeleteNodeDialog, ProxmoxNodeModalStack, NodeModal shell, TrueNASSettingsPanel, VMwareSettingsPanel, useProxmoxDirectWorkspaceState); lifecycle extensions must route through the unified aggregator ledger, source-manager cards, and ConnectionEditor credential slots rather than reintroducing those retired surfaces.
Those lifecycle-owned settings hooks may consume websocket state only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx` or recreate root-shell providers.
Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the infrastructure source-management toolbar too: the landing page exposes `Add infrastructure`, `Detect address`, `Install agent`, `Run discovery`, and `Discovery settings` as first-viewport actions inside the source manager rather than splitting first-run guidance from ongoing controls. The same landing boundary may surface setup confidence from the unified rows and discovered candidates, including connected-system count, API coverage, agent coverage, sources that still need an agent, and discovery review state, without creating a second inventory model or provider-specific summary fetch. Source groups must stay in the governed source catalog order instead of re-sorting by current row count, row-level lifecycle entry points must use `Manage` language, and locked agent-install states must show a compact command inventory without raw token placeholders or disabled copy commands until a token exists. Network discovery settings remain safety-critical: automatic scanning must surface the shared-network/subnet warning before operators save scan mode changes.
Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the infrastructure source-management toolbar too: the landing page exposes `Add infrastructure`, `Run discovery`, and `Discovery settings` as first-viewport toolbar actions inside the source manager, while governed source rows expose per-source add actions such as `Install Pulse Agent` from the shared catalog. `Detect address` remains inside the single add-flow source picker/probe path rather than a duplicate toolbar action. The same landing boundary may surface setup confidence from the unified rows and discovered candidates, including connected-system count, API coverage, agent coverage, sources that still need an agent, and discovery review state, without creating a second inventory model or provider-specific summary fetch. Source groups must stay in the governed source catalog order instead of re-sorting by current row count, row-level lifecycle entry points must use `Manage` language, and locked agent-install states must show a compact command inventory without raw token placeholders or disabled copy commands until a token exists. Network discovery settings remain safety-critical: automatic scanning must surface the shared-network/subnet warning before operators save scan mode changes.
Setup-completion handoff belongs to that same single add-flow boundary. The first-run completion screen must keep credentials as the first surfaced object, then present one compact next-step surface that sends operators to Add infrastructure or directly to the Agent handoff; source-choice explanation may live inside that surface, but lifecycle work must not reintroduce a separate setup-wizard tour, duplicate CTA section, or inline install-command owner before the canonical infrastructure workspace.
The same lifecycle-owned workspace boundary now also owns attached-agent composition. When a unified Pulse Agent augments a first-class platform source such as Proxmox VE, the source manager and edit dialog must present one primary platform row with explicit `API` plus `Pulse Agent` composition rather than duplicating that same machine as a second peer row under a generic Pulse Agent platform bucket. Standalone hosts with no owning platform source remain grouped under a standalone-host owner bucket, with `Pulse Agent` shown as the collection method rather than as the pseudo-platform label.
When that primary Proxmox source is cluster-backed, the same workspace boundary must render the row under the canonical cluster moniker carried by the backend grouping contract rather than under one sibling node's hostname. That same backend grouping contract must also carry the explicit member-node list for the cluster so the source manager can render child node composition such as `delly` and `minipc` beneath the owning cluster row with per-node coverage/status. Cluster-member node agents belong as augmentations on that owning Proxmox row and its child nodes, not as separate standalone-host peers.

View file

@ -258,10 +258,13 @@ work extends shared components instead of creating new local variants.
and no redundant monitored-systems ledger beneath it. The landing route may
include a dedicated first-viewport toolbar that explains platform APIs and
Pulse Agent telemetry as infrastructure sources and exposes `Add
infrastructure`, `Detect address`, `Install agent`, `Run discovery`, and
`Discovery settings` inside the source manager. It may also show a compact
coverage strip derived from the same unified connection rows and discovered
candidates so operators can confirm connected-system count, API coverage,
infrastructure`, `Run discovery`, and `Discovery settings` inside the
source manager. Per-source add actions, including `Install Pulse Agent`,
belong on the governed source rows, and `Detect address` stays inside the
single add-flow probe path instead of a duplicate toolbar action. It may
also show a compact coverage strip derived from the same unified connection
rows and discovered candidates so operators can confirm connected-system
count, API coverage,
agent coverage, sources that still need an agent, and discovery review state
without opening a tour or second ledger. Existing sources stay visible in
stable source-catalog order, and add, detect, install, review, and manage

View file

@ -26,6 +26,7 @@ import {
} from './connectionsTableModel';
import type { DiscoveredServer, DiscoveryScanStatus } from './infrastructureSettingsModel';
import {
getInfrastructureCoverageCompleteActionPresentation,
getInfrastructureEmptyStateDetail,
getInfrastructureEmptyStateSummary,
getInfrastructureOnboardingProductPresentation,
@ -41,7 +42,6 @@ interface InfrastructureSourceManagerProps {
readOnly: boolean;
onAddSource?: (type: InfrastructureOnboardingConnectionType) => void;
onAddInfrastructure?: () => void;
onDetectFromAddress?: () => void;
onRunDiscovery?: () => void;
onOpenDiscoverySettings?: () => void;
onOpenConnection?: (row: InfrastructureSystemRow) => void;
@ -250,16 +250,6 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
}
if (connectedSystemCount() === 0) {
if (props.onDetectFromAddress) {
return {
kind: 'detect',
label: 'Detect first source',
detail:
'Probe an address so Pulse can identify the platform and open the right setup flow.',
onClick: props.onDetectFromAddress,
};
}
return {
kind: 'add',
label: 'Choose source type',
@ -289,12 +279,11 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
};
}
const completeAction = getInfrastructureCoverageCompleteActionPresentation();
return {
kind: 'add',
label: 'Add another source',
detail:
'Coverage looks coherent for the connected systems. Add another source when the estate changes.',
onClick: props.onAddInfrastructure,
label: completeAction.label,
detail: completeAction.detail,
};
});
@ -352,56 +341,36 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
</button>
</Show>
<Show when={props.onDetectFromAddress}>
<button
type="button"
onClick={props.onDetectFromAddress}
class={workspaceSecondaryButtonClass}
>
<Search class="h-4 w-4" />
Detect address
</button>
</Show>
<div class="ml-auto flex flex-wrap items-center gap-2">
<Show when={props.onRunDiscovery}>
<button
type="button"
onClick={props.onRunDiscovery}
disabled={props.discoveryScanStatus().scanning}
class={utilityToolbarButtonClass}
aria-label="Run discovery"
title="Run discovery"
>
<RotateCw
class={`h-4 w-4 ${props.discoveryScanStatus().scanning ? 'animate-spin' : ''}`}
/>
{props.discoveryScanStatus().scanning ? 'Scanning…' : 'Run discovery'}
</button>
</Show>
<Show when={props.onAddSource}>
<button
type="button"
onClick={() => props.onAddSource?.('agent')}
class={workspaceSecondaryButtonClass}
>
<Cpu class="h-4 w-4" />
Install agent
</button>
</Show>
<Show when={props.onRunDiscovery}>
<button
type="button"
onClick={props.onRunDiscovery}
disabled={props.discoveryScanStatus().scanning}
class={utilityToolbarButtonClass}
aria-label="Run discovery"
title="Run discovery"
>
<RotateCw
class={`h-4 w-4 ${props.discoveryScanStatus().scanning ? 'animate-spin' : ''}`}
/>
{props.discoveryScanStatus().scanning ? 'Scanning…' : 'Run discovery'}
</button>
</Show>
<Show when={props.onOpenDiscoverySettings}>
<button
type="button"
onClick={props.onOpenDiscoverySettings}
class={utilityToolbarButtonClass}
aria-label="Discovery settings"
title="Discovery settings"
>
<SlidersHorizontal class="h-4 w-4" />
Settings
</button>
</Show>
<Show when={props.onOpenDiscoverySettings}>
<button
type="button"
onClick={props.onOpenDiscoverySettings}
class={utilityToolbarButtonClass}
aria-label="Discovery settings"
title="Discovery settings"
>
<SlidersHorizontal class="h-4 w-4" />
Discovery settings
</button>
</Show>
</div>
</div>
</div>
</Show>
@ -565,25 +534,23 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
}
>
<TableRow class={groupRowClass()}>
<TableCell colspan={5} class="px-3 py-1.5">
<div class="flex items-center gap-2 whitespace-nowrap">
<TableCell colspan={6} class="px-3 py-1.5">
<div class="flex items-center justify-between gap-2 whitespace-nowrap">
<span class={groupLabelClass()}>{product.label}</span>
<Show when={!props.readOnly && props.onAddSource}>
<button
type="button"
onClick={() => props.onAddSource?.(product.type)}
class={`${addSectionButtonClass} whitespace-nowrap`}
aria-label={product.actionLabel}
title={product.actionLabel}
>
<Plus class="h-3.5 w-3.5" />
{product.actionLabel}
</button>
</Show>
</div>
</TableCell>
<TableCell class="px-3 py-1.5 text-right">
<Show when={!props.readOnly && props.onAddSource}>
<button
type="button"
onClick={() => props.onAddSource?.(product.type)}
class={`${addSectionButtonClass} whitespace-nowrap`}
aria-label={product.actionLabel}
title={product.actionLabel}
>
<Plus class="h-3.5 w-3.5" />
Add
</button>
</Show>
</TableCell>
</TableRow>
</Show>
@ -931,7 +898,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
title={product.actionLabel}
>
<Plus class="h-3.5 w-3.5" />
Add
{product.actionLabel}
</button>
</Show>
</header>

View file

@ -858,6 +858,11 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
if (!connection) return 'Update source state, credentials, and lifecycle actions here.';
const label = describeManagedSourceType(connection.type);
const methods = attachedAgentConnections().length > 0 ? ' · API + Pulse Agent' : '';
const row = editingRow();
if (row?.isCluster) {
const contact = connection.name || connection.address || 'contact node';
return `${label} cluster · Editing via contact node ${contact}${connection.address ? ` (${connection.address})` : ''}${methods}`;
}
return `${label}${connection.address ? ` · ${connection.address}` : ''}${methods}`;
});
@ -877,7 +882,6 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
: (type) => openAddFlow(type === 'agent' ? 'agent' : (type as ManagedAddTypeStep))
}
onAddInfrastructure={readOnly() ? undefined : () => openAddFlow('pick')}
onDetectFromAddress={readOnly() ? undefined : () => openAddFlow('detect')}
onRunDiscovery={
readOnly()
? undefined

View file

@ -293,8 +293,8 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByRole('button', { name: /Run discovery/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Discovery settings/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^Add infrastructure$/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Detect address/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^Install agent$/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /^Detect address$/i })).toBeNull();
expect(screen.queryByRole('button', { name: /^Install agent$/i })).toBeNull();
const readiness = screen.getByRole('region', {
name: /Infrastructure setup confidence/i,
});
@ -315,7 +315,7 @@ describe('InfrastructureWorkspace', () => {
expect(screen.queryByText('Standalone hosts')).toBeNull();
expect(screen.getByRole('button', { name: /Add Proxmox VE/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Add TrueNAS SCALE/i })).toBeNull();
expect(screen.queryByRole('button', { name: /Add host/i })).toBeNull();
expect(screen.queryByRole('button', { name: /Install Pulse Agent/i })).toBeNull();
expect(screen.getByRole('button', { name: /Manage/i })).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Monitored systems' })).not.toBeInTheDocument();
});
@ -325,16 +325,6 @@ describe('InfrastructureWorkspace', () => {
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: /Detect address/i }));
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=detect', {
scroll: false,
});
fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i }));
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=agent', {
scroll: false,
});
fireEvent.click(
within(
screen.getByRole('region', {

View file

@ -174,8 +174,8 @@ describe('settings architecture guardrails', () => {
expect(infrastructureSourceManagerSource).toContain('aria-label={product.actionLabel}');
expect(infrastructureSourceManagerSource).toContain('Review');
expect(infrastructureSourceManagerSource).toContain('Manage');
expect(infrastructureSourceManagerSource).toContain('Detect address');
expect(infrastructureSourceManagerSource).toContain('Install agent');
expect(infrastructureSourceManagerSource).not.toContain('Detect address');
expect(infrastructureSourceManagerSource).not.toContain("'Install agent'");
expect(infrastructureSourceManagerSource).toContain('Choose source type');
expect(infrastructureSourceManagerSource).toContain('getInfrastructureEmptyStateSummary');
expect(infrastructureSourceManagerSource).toContain('Infrastructure coverage');

View file

@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
getInfrastructureCoverageCompleteActionPresentation,
getInfrastructureApiProductsByGovernanceState,
getInfrastructureAutoDetectLabels,
getInfrastructureEmptyStateDetail,
@ -90,7 +91,7 @@ describe('infrastructureOnboardingPresentation', () => {
expect.objectContaining({
type: 'agent',
label: 'Standalone hosts',
actionLabel: 'Add host',
actionLabel: 'Install Pulse Agent',
}),
]);
@ -169,4 +170,11 @@ describe('infrastructureOnboardingPresentation', () => {
expect(getInfrastructureEmptyStateDetail()).toContain('standalone hosts through Pulse Agent');
expect(getInfrastructureEmptyStateDetail()).toContain('Docker and Kubernetes are discovered');
});
it('owns the source-manager coverage-complete copy outside the component', () => {
expect(getInfrastructureCoverageCompleteActionPresentation()).toEqual({
label: 'Coverage coherent',
detail: 'Coverage looks coherent for the connected systems.',
});
});
});

View file

@ -58,6 +58,11 @@ export interface InfrastructureOnboardingPathPresentation {
coverage: string;
}
export interface InfrastructureCoverageCompleteActionPresentation {
label: string;
detail: string;
}
const SOURCE_STRATEGY_PRESENTATION: Record<
InfrastructureSourceStrategy,
InfrastructureSourceStrategyPresentation
@ -186,7 +191,7 @@ const SOURCE_MANAGER_LABEL_OVERRIDES: Partial<
> = {
agent: {
label: 'Standalone hosts',
actionLabel: 'Add host',
actionLabel: 'Install Pulse Agent',
},
};
@ -323,3 +328,9 @@ export const getInfrastructureEmptyStateSummary = (): string =>
export const getInfrastructureEmptyStateDetail = (): string =>
'Supported source types include VMware vCenter, TrueNAS SCALE, Proxmox VE, Proxmox Backup Server, Proxmox Mail Gateway, and standalone hosts through Pulse Agent. Docker and Kubernetes are discovered from supported agent hosts.';
export const getInfrastructureCoverageCompleteActionPresentation =
(): InfrastructureCoverageCompleteActionPresentation => ({
label: 'Coverage coherent',
detail: 'Coverage looks coherent for the connected systems.',
});