diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index d2203f8e39..435c79cabc 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -145,8 +145,11 @@ management, and fleet control surfaces. 6. Keep `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx` oriented around the first monitored host. Install-token generation, governed command copy, and install instructions belong to the canonical - lifecycle path; transport details and adjacent alternatives must remain - secondary to that first-host onboarding narrative. + lifecycle path; transport details, trust overrides, profile tuning, and + adjacent alternatives must remain secondary to that first-host onboarding + narrative, including an explicit advanced-options disclosure so first-time + operators see token generation, command copy, and status confirmation + before non-default connection controls. 7. Keep `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` and `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts` aligned with that same lifecycle path. Bare infrastructure settings routes diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx index 2d0e2b77d2..ab9e53cfc2 100644 --- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx @@ -1,5 +1,5 @@ import type { Component } from 'solid-js'; -import { For, Show } from 'solid-js'; +import { For, Show, createSignal } from 'solid-js'; import Server from 'lucide-solid/icons/server'; import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon'; import SettingsPanel from '@/components/shared/SettingsPanel'; @@ -22,6 +22,7 @@ import { useInfrastructureOperationsContext } from './useInfrastructureOperation export const InfrastructureInstallerSection: Component = () => { const state = useInfrastructureOperationsContext(); + const [showAdvancedOptions, setShowAdvancedOptions] = createSignal(false); return ( { Installation commands

- The installer auto-detects Docker, Kubernetes, and Proxmox on the target - machine. + Copy the default command for the first host first. Open advanced options only if + this machine needs custom connection or install settings.

- -
- state.setCustomAgentUrl(event.currentTarget.value)} - placeholder={state.agentUrl()} - class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800" - /> +
+
+

+ Advanced connection and install options +

+

+ Default commands use the auto-detected Pulse URL. Open this only if the target + host needs a different URL, custom CA trust, TLS override, command execution, + or a specific install profile. +

+
+
-

- Override the address agents use to connect to this server (e.g., use IP address{' '} - http://192.0.2.50:7655 if DNS fails). - - Currently using auto-detected: {state.agentUrl()} +

+ + +
+
+ +
+ state.setCustomAgentUrl(event.currentTarget.value)} + placeholder={state.agentUrl()} + class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800" + /> +
+

+ Override the address agents use to connect to this server (e.g., use IP address{' '} + http://192.0.2.50:7655 if DNS fails). + + + Currently using auto-detected: {state.agentUrl()} + + +

+
+ +
+ +
+ state.setCustomCaPath(event.currentTarget.value)} + placeholder={ + state.selectedAgentUrl().startsWith('http://') + ? 'Not needed for plain HTTP' + : 'Examples: /etc/pulse/ca.pem or C:\\Pulse\\ca.cer' + } + class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800" + /> +
+

+ Preserves custom trust for copied install, upgrade, and uninstall commands. + Shell commands pass --cacert to both the download and the installer. + Windows commands set PULSE_CACERT and use a transport-aware + PowerShell bootstrap for the initial script fetch. +

+
+ + +
+ TLS verification disabled — skip cert checks for + self-signed setups. Not recommended for production. +
-

-
-
- -
- state.setCustomCaPath(event.currentTarget.value)} - placeholder={ - state.selectedAgentUrl().startsWith('http://') - ? 'Not needed for plain HTTP' - : 'Examples: /etc/pulse/ca.pem or C:\\Pulse\\ca.cer' - } - class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800" - /> -
-

- Preserves custom trust for copied install, upgrade, and uninstall commands. - Shell commands pass --cacert to both the download and the installer. - Windows commands set PULSE_CACERT and use a transport-aware - PowerShell bootstrap for the initial script fetch. -

-
+ - -
- TLS verification disabled — skip cert checks for - self-signed setups. Not recommended for production. + + + +
+ Pulse commands enabled — The agent will accept + diagnostic and fix commands from Pulse Patrol features. +
+
+ +
+ Config signing (optional) — Require signed remote + config payloads with PULSE_AGENT_CONFIG_SIGNATURE_REQUIRED=true. + Provide keys via PULSE_AGENT_CONFIG_SIGNING_KEY (Pulse) and{' '} + PULSE_AGENT_CONFIG_PUBLIC_KEYS (agents). +
+ +
+ + +

+ {state.getSelectedInstallProfile().description} +

+ 0}> +

+ Adds flags to shell-based install commands:{' '} + {state.getInstallProfileFlags().join(' ')} +

+
+
- - - - - - -
- Pulse commands enabled — The agent will accept - diagnostic and fix commands from Pulse Patrol features. -
-
- -
- Config signing (optional) — Require signed remote - config payloads with PULSE_AGENT_CONFIG_SIGNATURE_REQUIRED=true. - Provide keys via PULSE_AGENT_CONFIG_SIGNING_KEY (Pulse) and{' '} - PULSE_AGENT_CONFIG_PUBLIC_KEYS (agents). -
- -
- - -

{state.getSelectedInstallProfile().description}

- 0}> -

- Adds flags to shell-based install commands:{' '} - {state.getInstallProfileFlags().join(' ')} -

-
-
diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx index 6da1178d00..6780821edf 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx @@ -115,6 +115,10 @@ describe('InfrastructureOperationsController ownership guardrails', () => { 'SettingsAPI.updateSystemSettings', ); expect(infrastructureInstallerSectionSource).toContain('useInfrastructureOperationsContext'); + expect(infrastructureInstallerSectionSource).toContain('Advanced connection and install options'); + expect(infrastructureInstallerSectionSource).toContain( + 'Show advanced connection and install options', + ); expect(infrastructureInventorySectionSource).toContain('useInfrastructureOperationsContext'); expect(infrastructureStopMonitoringDialogSource).toContain('useInfrastructureOperationsContext'); expect(infrastructureOperationsModelSource).toContain('export const getRowReportingSummary'); @@ -564,6 +568,7 @@ const setupWithResources = (resources: any[], connectedInfrastructure: Connected }; const getConnectionUrlInput = (): HTMLInputElement => { + openAdvancedInstallOptions(); const urlBlock = screen.getByText(/Connection URL \(Agent → Pulse\)/i).closest('div'); expect(urlBlock).not.toBeNull(); const urlInput = urlBlock?.querySelector('input'); @@ -571,6 +576,35 @@ const getConnectionUrlInput = (): HTMLInputElement => { return urlInput as HTMLInputElement; }; +const openAdvancedInstallOptions = (): void => { + const toggle = screen.getByRole('button', { + name: /Show advanced connection and install options|Hide advanced connection and install options/i, + }); + if ((toggle.textContent ?? '').includes('Show advanced')) { + fireEvent.click(toggle); + } +}; + +const getCustomCaPathInput = (): HTMLElement => { + openAdvancedInstallOptions(); + return screen.getByLabelText(/Custom CA certificate path \(optional\)/i); +}; + +const getSkipTlsCheckbox = (): HTMLElement => { + openAdvancedInstallOptions(); + return screen.getByRole('checkbox', { name: /Skip TLS certificate verification/i }); +}; + +const getEnableCommandsCheckbox = (): HTMLElement => { + openAdvancedInstallOptions(); + return screen.getByRole('checkbox', { name: /Enable Pulse command execution/i }); +}; + +const getTargetProfileSelect = (): HTMLElement => { + openAdvancedInstallOptions(); + return screen.getByLabelText('Target profile (optional)'); +}; + beforeEach(() => { securityStatusResponse = { requiresAuth: true, apiTokenConfigured: false }; navigateMock.mockReset(); @@ -657,6 +691,36 @@ describe('InfrastructureOperationsController token generation', () => { 4000, ); }); + + it('keeps advanced connection options collapsed until operators ask for them', async () => { + createTokenMock.mockResolvedValue({ + token: 'test-token-123', + record: { + id: 'token-record', + name: 'Test Token', + prefix: 'abcdef', + suffix: '1234', + createdAt: new Date().toISOString(), + }, + }); + + setupComponent(); + + fireEvent.click(screen.getByRole('button', { name: /Generate token/i })); + await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); + + expect( + screen.getByRole('button', { name: /Show advanced connection and install options/i }), + ).toBeInTheDocument(); + expect(screen.queryByText(/Connection URL \(Agent → Pulse\)/i)).not.toBeInTheDocument(); + + openAdvancedInstallOptions(); + + expect(screen.getByText(/Connection URL \(Agent → Pulse\)/i)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Hide advanced connection and install options/i }), + ).toBeInTheDocument(); + }); }); describe('InfrastructureOperationsController agent lookup', () => { @@ -1713,12 +1777,11 @@ describe('InfrastructureOperationsController managed agents table', () => { fireEvent.click(generateButton); await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); + openAdvancedInstallOptions(); await waitFor(() => { - expect( - screen.getByRole('checkbox', { name: /Skip TLS certificate verification/i }), - ).toBeInTheDocument(); + expect(getSkipTlsCheckbox()).toBeInTheDocument(); }); - fireEvent.click(screen.getByRole('checkbox', { name: /Skip TLS certificate verification/i })); + fireEvent.click(getSkipTlsCheckbox()); const toggle = screen.getByRole('button', { name: /details for Windows Self-Signed Host/i }); fireEvent.click(toggle); @@ -2228,11 +2291,11 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); await waitFor(() => { - expect(screen.getByText(/Skip TLS certificate verification/i)).toBeInTheDocument(); + expect(getSkipTlsCheckbox()).toBeInTheDocument(); }); // TLS skip is disabled by default - const checkbox = screen.getByRole('checkbox', { name: /Skip TLS certificate verification/i }); + const checkbox = getSkipTlsCheckbox(); expect(checkbox).not.toBeChecked(); // Enable it @@ -2263,10 +2326,10 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); await waitFor(() => { - expect(screen.getByLabelText('Target profile (optional)')).toBeInTheDocument(); + expect(getTargetProfileSelect()).toBeInTheDocument(); }); - const profileSelect = screen.getByLabelText('Target profile (optional)'); + const profileSelect = getTargetProfileSelect(); fireEvent.change(profileSelect, { target: { value: 'proxmox-pbs' } }); expect(trackAgentInstallProfileSelectedMock).toHaveBeenCalledWith( @@ -2298,10 +2361,10 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); await waitFor(() => { - expect(screen.getByLabelText('Target profile (optional)')).toBeInTheDocument(); + expect(getTargetProfileSelect()).toBeInTheDocument(); }); - const profileSelect = screen.getByLabelText('Target profile (optional)'); + const profileSelect = getTargetProfileSelect(); fireEvent.change(profileSelect, { target: { value: 'proxmox-pbs' } }); const windowsHeading = screen.getByText('Install on Windows'); @@ -2340,10 +2403,10 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); await waitFor(() => { - expect(screen.getByLabelText('Target profile (optional)')).toBeInTheDocument(); + expect(getTargetProfileSelect()).toBeInTheDocument(); }); - const profileSelect = screen.getByLabelText('Target profile (optional)'); + const profileSelect = getTargetProfileSelect(); fireEvent.change(profileSelect, { target: { value: 'docker' } }); expect(trackAgentInstallProfileSelectedMock).toHaveBeenCalledWith( @@ -2484,15 +2547,13 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); await waitFor(() => { - expect(screen.getByLabelText('Target profile (optional)')).toBeInTheDocument(); + expect(getTargetProfileSelect()).toBeInTheDocument(); }); - const profileSelect = screen.getByLabelText('Target profile (optional)'); + const profileSelect = getTargetProfileSelect(); fireEvent.change(profileSelect, { target: { value: 'docker' } }); - const enableCommandsCheckbox = screen.getByRole('checkbox', { - name: /Enable Pulse command execution/i, - }); + const enableCommandsCheckbox = getEnableCommandsCheckbox(); fireEvent.click(enableCommandsCheckbox); const copyButton = screen.getAllByRole('button', { name: /Copy command/i })[0]; @@ -2594,7 +2655,7 @@ describe('InfrastructureOperationsController platform commands', () => { fireEvent.click(screen.getByRole('button', { name: /Generate token/i })); await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); - fireEvent.input(screen.getByLabelText(/Custom CA certificate path \(optional\)/i), { + fireEvent.input(getCustomCaPathInput(), { target: { value: '/etc/pulse/custom-ca.pem' }, }); @@ -2627,11 +2688,11 @@ describe('InfrastructureOperationsController platform commands', () => { fireEvent.input(urlInput, { target: { value: 'http://pulse.example:7655/' }, }); - fireEvent.input(screen.getByLabelText(/Custom CA certificate path \(optional\)/i), { + fireEvent.input(getCustomCaPathInput(), { target: { value: '/etc/pulse/custom-ca.pem' }, }); - fireEvent.click(screen.getByRole('checkbox', { name: /Enable Pulse command execution/i })); - fireEvent.change(screen.getByLabelText('Target profile (optional)'), { + fireEvent.click(getEnableCommandsCheckbox()); + fireEvent.change(getTargetProfileSelect(), { target: { value: 'docker' }, }); @@ -2668,12 +2729,12 @@ describe('InfrastructureOperationsController platform commands', () => { fireEvent.click(screen.getByRole('button', { name: /Generate token/i })); await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); - fireEvent.input(screen.getByLabelText(/Custom CA certificate path \(optional\)/i), { + fireEvent.input(getCustomCaPathInput(), { target: { value: 'C:\\Pulse\\custom-ca.cer' }, }); - fireEvent.click(screen.getByRole('checkbox', { name: /Skip TLS certificate verification/i })); - fireEvent.click(screen.getByRole('checkbox', { name: /Enable Pulse command execution/i })); - fireEvent.change(screen.getByLabelText('Target profile (optional)'), { + fireEvent.click(getSkipTlsCheckbox()); + fireEvent.click(getEnableCommandsCheckbox()); + fireEvent.change(getTargetProfileSelect(), { target: { value: 'proxmox-pbs' }, }); @@ -2774,14 +2835,10 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); - const insecureCheckbox = screen.getByRole('checkbox', { - name: /Skip TLS certificate verification/i, - }); + const insecureCheckbox = getSkipTlsCheckbox(); fireEvent.click(insecureCheckbox); - const enableCommandsCheckbox = screen.getByRole('checkbox', { - name: /Enable Pulse command execution/i, - }); + const enableCommandsCheckbox = getEnableCommandsCheckbox(); fireEvent.click(enableCommandsCheckbox); const windowsHeading = screen.getByText('Install on Windows'); @@ -3009,7 +3066,7 @@ describe('InfrastructureOperationsController platform commands', () => { fireEvent.click(screen.getByRole('button', { name: /Generate token/i })); await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); - fireEvent.input(screen.getByLabelText(/Custom CA certificate path \(optional\)/i), { + fireEvent.input(getCustomCaPathInput(), { target: { value: 'C:\\Pulse\\custom-ca.cer' }, }); @@ -3063,14 +3120,10 @@ describe('InfrastructureOperationsController platform commands', () => { await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 }); - const insecureCheckbox = screen.getByRole('checkbox', { - name: /Skip TLS certificate verification/i, - }); + const insecureCheckbox = getSkipTlsCheckbox(); fireEvent.click(insecureCheckbox); - const enableCommandsCheckbox = screen.getByRole('checkbox', { - name: /Enable Pulse command execution/i, - }); + const enableCommandsCheckbox = getEnableCommandsCheckbox(); fireEvent.click(enableCommandsCheckbox); fireEvent.click(screen.getByRole('button', { name: /details for Windows Upgrade Env/i })); @@ -3240,10 +3293,6 @@ describe('InfrastructureOperationsController platform commands', () => { fireEvent.click(screen.getByRole('button', { name: /Confirm without token/i })); - await waitFor(() => { - expect(screen.getByText(/Connection URL \(Agent → Pulse\)/i)).toBeInTheDocument(); - }); - const urlInput = getConnectionUrlInput(); fireEvent.input(urlInput, { target: { value: 'http://pulse.example:7655' }, @@ -3276,7 +3325,7 @@ describe('InfrastructureOperationsController platform commands', () => { }); fireEvent.click(screen.getByRole('button', { name: /Confirm without token/i })); - fireEvent.input(screen.getByLabelText(/Custom CA certificate path \(optional\)/i), { + fireEvent.input(getCustomCaPathInput(), { target: { value: '/etc/pulse/upgrade-ca.pem' }, });