Refine infrastructure onboarding flows

This commit is contained in:
rcourtman 2026-04-23 21:55:09 +01:00
parent 643db3f378
commit 485bac636b
10 changed files with 880 additions and 688 deletions

View file

@ -116,6 +116,12 @@ management, and fleet control surfaces.
22. `scripts/install.ps1` shared with `deployment-installability`: the Windows installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary.
23. `scripts/install.sh` shared with `deployment-installability`: the shell installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary.
The node setup modal boundary must keep guided setup and manual credential
submission separate. For new PVE/PBS setup, Agent Install and Direct Connection
setup-script modes are command-driven auto-registration paths; Token ID/Value
fields, Test Connection, and Add Node submission belong only to Manual Token
Setup or existing-node edit flows.
That shared monitored-system admission preview boundary also owns the disabled
platform-connection lifecycle state. Once a TrueNAS or VMware setup form marks
the connection disabled, lifecycle surfaces must treat a canonical zero-delta
@ -227,7 +233,7 @@ an add-only capacity posture.
Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes.
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 add-flow entry split too: the landing strip opens the grouped `Add infrastructure` picker, while `Detect from address` remains a picker-owned utility route instead of a competing top-level header action.
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 add-flow entry split too: the landing guidance exposes `Detect from address`, `Install Pulse Agent`, and `Choose source type` as first-run actions above the source ledger, while header actions stay limited to ongoing discovery controls.
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.
That same lifecycle-owned workspace boundary also owns agent version

View file

@ -123,6 +123,11 @@ Own canonical runtime payload shapes between backend and frontend.
29. `frontend-modern/src/utils/apiTokenPresentation.ts` shared with `security-privacy`: the API token presentation helper is both a security/privacy control surface and a canonical API token management boundary.
30. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` shared with `agent-lifecycle`: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary.
31. `internal/api/access_control_handlers.go` shared with `organization-settings`: RBAC role and user-assignment handlers are both an organization settings control surface and a canonical API payload contract boundary.
The shared node setup boundary above owns the guided/manual setup split
for PVE/PBS consumers: Agent Install and Direct Connection setup-script
modes are auto-registration paths, while Token ID/Value fields, Test
Connection, and Add Node are manual-token or existing-node edit controls
only.
32. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary.
33. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary.
34. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary.
@ -2281,6 +2286,11 @@ masked: copy-success messaging may not tell the operator to paste a token
"shown below" once only `tokenHint` remains visible, and stale raw-token
cleanup paths may not survive in one Proxmox branch after the shared UI state
has moved to hint-only handling.
That same shared settings consumer must keep command-driven setup and manual
credential submission distinct. When a new PVE/PBS setup is in Agent Install or
Direct Connection setup-script mode, the settings UI must not render Token
ID/Value fields, Test Connection, or Add Node controls; those controls are only
valid for Manual Token Setup or existing-node edit flows.
That same shared frontend setup surface must also trim and validate the
canonical `host` input before invoking `/api/setup-script` downloads, and the
shared `frontend-modern/src/api/nodes.ts` helper must reject empty `host` or

View file

@ -245,15 +245,16 @@ work extends shared components instead of creating new local variants.
from a top-level product route such as `/recovery`, the first highlighted
step should match that route instead of always restarting at Dashboard.
5. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell, but setup guidance may still use `Platform connections` as the operator-facing label for the shared API-backed onboarding path.
That same shared shell boundary now owns the default posture for
That same shared shell boundary now owns the first-run posture for
`/settings/infrastructure`: the landing route should read as one
source-manager workspace with configured infrastructure instances first
and no redundant monitored-systems ledger beneath it. The landing route
must not lead with
connection-type explanations, onboarding-path copy, or separate detect
utilities. Existing sources stay visible on the page, and add, detect,
install, and edit flows open as secondary interactions from that same
destination instead of taking over the whole page.
and no redundant monitored-systems ledger beneath it. The landing route may
include a compact guidance strip that explains platform APIs and host agents
as Pulse 6 infrastructure sources and exposes `Detect from address`, `Install
Pulse Agent`, and `Choose source type` as first-run actions. Existing sources
stay visible on the page, and add, detect, install, and edit flows open as
secondary interactions from that same destination instead of taking over the
whole page.
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

@ -875,6 +875,7 @@ 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

@ -22,12 +22,7 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
return (
<div>
<SectionHeader
title="Authentication"
size="sm"
class="mb-4"
titleClass="text-base-content"
/>
<SectionHeader title="Authentication" size="sm" class="mb-4" titleClass="text-base-content" />
<div class="mb-4">
<div class="flex gap-4">
@ -62,8 +57,8 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
<Show when={modalProps.nodeType === 'pmg'}>
<p class="text-xs text-muted mt-2">
Proxmox Mail Gateway does not support API tokens. Use a service account with password
authentication (for example <code>root@pam</code> or a dedicated{' '}
<code>api@pmg</code> user).
authentication (for example <code>root@pam</code> or a dedicated <code>api@pmg</code>{' '}
user).
</p>
</Show>
</div>
@ -98,7 +93,9 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
type="password"
value={state.formData().password}
onInput={(event) => state.updateField('password', event.currentTarget.value)}
placeholder={state.isEditingExistingNode() ? 'Leave blank to keep existing' : 'Password'}
placeholder={
state.isEditingExistingNode() ? 'Leave blank to keep existing' : 'Password'
}
required={state.formData().authType === 'password' && !state.isEditingExistingNode()}
class={controlClass()}
/>
@ -110,44 +107,58 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
<div class="space-y-4">
<NodeModalSetupGuideSection modalProps={modalProps} state={state} />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class={formField}>
<label class={labelClass()}>
Token ID <span class="text-red-500">*</span>
</label>
<input
type="text"
value={state.formData().tokenName}
onInput={(event) => state.updateField('tokenName', event.currentTarget.value)}
placeholder={getNodeTokenIdPlaceholder(modalProps.nodeType)}
required={state.formData().authType === 'token'}
class={controlClass('font-mono')}
/>
<p class={formHelpText}>Full token ID from Proxmox (user@realm!tokenname).</p>
<Show when={state.formData().setupMode === 'manual'}>
<div class="rounded-md border border-border bg-surface-alt px-3 py-2 text-xs leading-5 text-muted">
Use the manual token path only when you already created the API token yourself. The
automatic setup paths register the node without copying token credentials into this
form.
</div>
<div class={formField}>
<label class={labelClass('flex items-center gap-2')}>
Token Value
<Show when={!state.isEditingExistingNode()}>
<span class="text-red-500">*</span>
</Show>
</label>
<input
type="password"
value={state.formData().tokenValue}
onInput={(event) => state.updateField('tokenValue', event.currentTarget.value)}
placeholder={
state.isEditingExistingNode()
? 'Leave blank to keep existing'
: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}
required={state.formData().authType === 'token' && !state.isEditingExistingNode()}
class={controlClass('font-mono')}
/>
<p class={formHelpText}>The secret value shown when creating the token.</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class={formField}>
<label class={labelClass()}>
Token ID <span class="text-red-500">*</span>
</label>
<input
type="text"
value={state.formData().tokenName}
onInput={(event) => state.updateField('tokenName', event.currentTarget.value)}
placeholder={getNodeTokenIdPlaceholder(modalProps.nodeType)}
required={
state.formData().authType === 'token' && state.formData().setupMode === 'manual'
}
class={controlClass('font-mono')}
/>
<p class={formHelpText}>Full token ID from Proxmox (user@realm!tokenname).</p>
</div>
<div class={formField}>
<label class={labelClass('flex items-center gap-2')}>
Token Value
<Show when={!state.isEditingExistingNode()}>
<span class="text-red-500">*</span>
</Show>
</label>
<input
type="password"
value={state.formData().tokenValue}
onInput={(event) => state.updateField('tokenValue', event.currentTarget.value)}
placeholder={
state.isEditingExistingNode()
? 'Leave blank to keep existing'
: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}
required={
state.formData().authType === 'token' &&
state.formData().setupMode === 'manual' &&
!state.isEditingExistingNode()
}
class={controlClass('font-mono')}
/>
<p class={formHelpText}>The secret value shown when creating the token.</p>
</div>
</div>
</div>
</Show>
</div>
</Show>
</div>

View file

@ -17,6 +17,15 @@ interface NodeModalStatusFooterProps {
export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (props) => {
const { modalProps, state } = props;
const guidedSetupOnlyMode = () =>
!state.isEditingExistingNode() &&
modalProps.nodeType !== 'pmg' &&
state.formData().authType === 'token' &&
state.formData().setupMode !== 'manual';
const guidedSetupFooterHint = () =>
state.formData().setupMode === 'agent'
? 'Run the generated command on the host. Pulse adds the node automatically after the agent starts.'
: 'Run the generated command on the host. Pulse adds the API connection automatically after the setup script finishes.';
return (
<>
@ -84,7 +93,9 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
<div class="mt-2 space-y-1">
<p class="text-xs font-semibold opacity-90">Warnings:</p>
<ul class="text-xs space-y-0.5 opacity-80">
<For each={state.testResult()?.warnings}>{(warning) => <li> {warning}</li>}</For>
<For each={state.testResult()?.warnings}>
{(warning) => <li> {warning}</li>}
</For>
</ul>
</div>
</Show>
@ -152,14 +163,21 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
: 'Delete connection'}
</button>
</Show>
<button
type="button"
onClick={state.handleTestConnection}
disabled={state.isTesting() || props.togglePending || props.deletePending}
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
<Show
when={!guidedSetupOnlyMode()}
fallback={
<p class="max-w-md text-xs leading-5 text-muted">{guidedSetupFooterHint()}</p>
}
>
{state.isTesting() ? 'Testing...' : 'Test Connection'}
</button>
<button
type="button"
onClick={state.handleTestConnection}
disabled={state.isTesting() || props.togglePending || props.deletePending}
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{state.isTesting() ? 'Testing...' : 'Test Connection'}
</button>
</Show>
</div>
<div class="flex items-center gap-3">
@ -208,15 +226,17 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
disabled={props.togglePending || props.deletePending}
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={props.togglePending || props.deletePending}
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{state.isEditingExistingNode() ? 'Update' : 'Add'} Node
{guidedSetupOnlyMode() ? 'Close' : 'Cancel'}
</button>
<Show when={!guidedSetupOnlyMode()}>
<button
type="submit"
disabled={props.togglePending || props.deletePending}
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{state.isEditingExistingNode() ? 'Update' : 'Add'} Node
</button>
</Show>
</div>
</div>
</>

View file

@ -287,9 +287,15 @@ describe('InfrastructureWorkspace', () => {
renderWorkspace();
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
expect(screen.getByText('Start by connecting what Pulse should monitor')).toBeInTheDocument();
expect(
screen.getByText(/Pulse 6 treats platform APIs and host agents as infrastructure sources/i),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Run discovery/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Discovery settings/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Detect from address/i })).toBeNull();
expect(screen.getByRole('button', { name: /Detect from address/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Choose source type/i })).toBeInTheDocument();
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
expect(screen.getByText('Proxmox VE').closest('tr')?.className).toContain('bg-base');
expect(screen.queryByText('VMware vCenter')).toBeNull();
@ -303,6 +309,27 @@ describe('InfrastructureWorkspace', () => {
expect(screen.queryByRole('heading', { name: 'Monitored systems' })).not.toBeInTheDocument();
});
it('routes first-run actions from the source manager guidance', async () => {
renderWorkspace();
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: /Detect from address/i }));
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=detect', {
scroll: false,
});
fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i }));
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=agent', {
scroll: false,
});
fireEvent.click(screen.getByRole('button', { name: /Choose source type/i }));
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=pick', {
scroll: false,
});
});
it('switches the source manager layout from measured container width during live resize', async () => {
installResizeObserverMock();
Object.defineProperty(window, 'innerWidth', {
@ -327,6 +354,22 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByText('Active')).toBeInTheDocument();
});
it('keeps onboarding copy visible in the empty infrastructure state', async () => {
connectionState.connections = [];
connectionState.rows = [];
renderWorkspace();
await waitFor(() =>
expect(screen.getByText('Start monitoring infrastructure')).toBeInTheDocument(),
);
expect(
screen.getByText('Add infrastructure systems to start monitoring your environment.'),
).toBeInTheDocument();
expect(screen.getByText(/Available system types: VMware vCenter/i)).toBeInTheDocument();
expect(screen.getByText(/standalone hosts through Pulse Agent/i)).toBeInTheDocument();
});
it('routes discovery actions from the manager and shows discovered candidates in the matching platform group', async () => {
const triggerDiscoveryScan = vi.fn();
renderWorkspace({

View file

@ -15,6 +15,9 @@ import connectionEditorSource from '../ConnectionEditor/ConnectionEditor.tsx?raw
import addressProbeStepSource from '../ConnectionEditor/AddressProbeStep.tsx?raw';
import connectionEditorStateSource from '../ConnectionEditor/useConnectionEditor.ts?raw';
import nodeCredentialSlotSource from '../ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx?raw';
import nodeModalAuthenticationSectionSource from '../NodeModalAuthenticationSection.tsx?raw';
import nodeModalStatusFooterSource from '../NodeModalStatusFooter.tsx?raw';
import nodeModalStateSource from '../useNodeModalState.ts?raw';
import trueNASCredentialSlotSource from '../ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx?raw';
import vmwareCredentialSlotSource from '../ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx?raw';
import diagnosticsResultsPanelSource from '../DiagnosticsResultsPanel.tsx?raw';
@ -168,7 +171,13 @@ describe('settings architecture guardrails', () => {
expect(infrastructureSourceManagerSource).toContain('aria-label={product.actionLabel}');
expect(infrastructureSourceManagerSource).toContain('Review');
expect(infrastructureSourceManagerSource).toContain('Edit');
expect(infrastructureSourceManagerSource).not.toContain('Detect from address');
expect(infrastructureSourceManagerSource).toContain(
'Start by connecting what Pulse should monitor',
);
expect(infrastructureSourceManagerSource).toContain('Detect from address');
expect(infrastructureSourceManagerSource).toContain('Install Pulse Agent');
expect(infrastructureSourceManagerSource).toContain('Choose source type');
expect(infrastructureSourceManagerSource).toContain('getInfrastructureEmptyStateSummary');
expect(infrastructureSourceManagerSource).not.toContain('Connection types');
expect(infrastructureSourcePickerSource).toContain('Detect from address');
expect(infrastructureSourcePickerSource).toContain('getInfrastructureSourcePickerGroups');
@ -230,6 +239,11 @@ describe('settings architecture guardrails', () => {
expect(nodeCredentialSlotSource).toContain('<NodeModalMonitoringSection');
expect(nodeCredentialSlotSource).toContain('<NodeModalStatusFooter');
expect(nodeCredentialSlotSource).not.toContain('<Dialog');
expect(nodeModalAuthenticationSectionSource).toContain(
"state.formData().setupMode === 'manual'",
);
expect(nodeModalStatusFooterSource).toContain('guidedSetupOnlyMode');
expect(nodeModalStateSource).toContain('data.setupMode !==');
expect(vmwareCredentialSlotSource).toContain('TlsVerificationWarningBanner');
expect(vmwareCredentialSlotSource).toContain('subject="this vCenter connection"');

View file

@ -96,10 +96,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
}
};
const copyProxmoxAgentInstallCommand = async (
type: 'pve' | 'pbs',
successMessage: string,
) => {
const copyProxmoxAgentInstallCommand = async (type: 'pve' | 'pbs', successMessage: string) => {
try {
setLoadingAgentCommand(true);
setAgentCommandError(null);
@ -356,6 +353,20 @@ export const useNodeModalState = (props: NodeModalProps) => {
event.preventDefault();
const data = formData();
if (
!isEditingExistingNode() &&
props.nodeType !== 'pmg' &&
data.authType === 'token' &&
data.setupMode !== 'manual'
) {
const setupPath =
data.setupMode === 'agent' ? 'Pulse Agent install command' : 'setup command';
notificationStore.info(
`Run the ${setupPath} on the host. Pulse will add the node automatically.`,
);
return;
}
const normalizedName = data.name.trim() || deriveNameFromHost(data.host);
if (!normalizedName) {
notificationStore.error('Node name is required');