diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index fd4c619c3b..8019c9fe94 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -147,6 +147,12 @@ management, and fleet control surfaces. 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. +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 + must default to the install workspace, and the workspace shell must make + the first-host sequence explicit before operators drift into reporting and + control surfaces. ## Current State diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 15011a9a3c..adb2b6a5a5 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -171,6 +171,12 @@ work extends shared components instead of creating new local variants. completion CTA language aligned with the governed infrastructure install workspace instead of regressing to a route jump that leaves the next action implicit. +8. Keep the settings-shell infrastructure landing path aligned with that same + first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` + must treat `/settings` and the infrastructure settings tab as the canonical + path to `/settings/infrastructure/install`, not to reporting/control, so + the shell does not send first-time operators to the wrong infrastructure + subview by default. ## Current State diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index 7601b12a00..f72e401bf4 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -20,6 +20,8 @@ export const InfrastructureWorkspace: Component = const navigate = useNavigate(); const location = useLocation(); const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname)); + const installPath = createMemo(() => buildInfrastructureWorkspacePath('install')); + const directPath = createMemo(() => buildInfrastructureWorkspacePath('direct')); const openView = (view: InfrastructureWorkspaceView) => navigate(buildInfrastructureWorkspacePath(view)); @@ -27,12 +29,66 @@ export const InfrastructureWorkspace: Component = return (
-
-

Infrastructure operations

-

- New to Pulse? Start with Install on a host to add your first monitored system. - If you prefer a direct integration instead, use Direct Proxmox. -

+
+
+

Connect your first system

+

+ Start with Install on a host to connect the first machine you want Pulse to monitor. + If you already know you want a direct integration instead, go straight to Direct + Proxmox. +

+
+
+
+

1. Choose path

+

+ Install Pulse on a host first, or use Direct Proxmox if that is the system you are + connecting. +

+
+
+

+ 2. Generate access +

+

+ Create the install token Pulse expects for the first monitored host, then copy the + generated command. +

+
+
+

+ 3. Confirm reporting +

+

+ Run the command on that machine, then use Reporting & control once the first + system starts reporting. +

+
+
+
+ + +

{SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral}

diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index b0f5ff962b..3cdbb3a643 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -30,7 +30,7 @@ vi.mock('../ProxmoxSettingsPanel', () => ({ describe('InfrastructureWorkspace', () => { beforeEach(() => { navigateSpy.mockReset(); - mockPathname = '/settings/infrastructure/operations'; + mockPathname = '/settings/infrastructure'; }); afterEach(() => { @@ -51,23 +51,26 @@ describe('InfrastructureWorkspace', () => { ) as any, ); - it('renders the canonical subtablist', () => { + it('defaults bare infrastructure routing to install on a host', () => { renderWorkspace(); const tablist = screen.getByRole('tablist', { name: 'Infrastructure workspace' }); expect(tablist).toBeInTheDocument(); - expect(screen.getByText('Infrastructure operations')).toBeInTheDocument(); + expect(screen.getByText('Connect your first system')).toBeInTheDocument(); expect( screen.getByText( - 'New to Pulse? Start with Install on a host to add your first monitored system. If you prefer a direct integration instead, use Direct Proxmox.', + 'Start with Install on a host to connect the first machine you want Pulse to monitor. If you already know you want a direct integration instead, go straight to Direct Proxmox.', ), ).toBeInTheDocument(); + expect(screen.getByText('1. Choose path')).toBeInTheDocument(); + expect(screen.getByText('2. Generate access')).toBeInTheDocument(); + expect(screen.getByText('3. Confirm reporting')).toBeInTheDocument(); expect( screen.getByText(SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral), ).toBeInTheDocument(); expect(screen.getByRole('tab', { name: 'Install on a host' })).toHaveAttribute( 'aria-selected', - 'false', + 'true', ); expect(screen.getByRole('tab', { name: 'Direct Proxmox' })).toHaveAttribute( 'aria-selected', @@ -75,8 +78,9 @@ describe('InfrastructureWorkspace', () => { ); expect(screen.getByRole('tab', { name: 'Reporting & control' })).toHaveAttribute( 'aria-selected', - 'true', + 'false', ); + expect(screen.getByTestId('unified-agents')).toBeInTheDocument(); }); it('uses the shared subtabs to switch to direct proxmox', () => { @@ -98,12 +102,33 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByTestId('proxmox-settings')).toBeInTheDocument(); }); + it('keeps the reporting route available for established operators', () => { + mockPathname = '/settings/infrastructure/operations'; + renderWorkspace(); + + expect(screen.getByRole('tab', { name: 'Reporting & control' })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect(screen.getByTestId('agent-profiles')).toBeInTheDocument(); + }); + + it('uses the guided workspace actions to open install and direct paths', () => { + renderWorkspace(); + + fireEvent.click(screen.getByRole('button', { name: 'Install on a host selected' })); + fireEvent.click(screen.getByRole('button', { name: 'Open Direct Proxmox' })); + + expect(navigateSpy).toHaveBeenNthCalledWith(1, '/settings/infrastructure/install'); + expect(navigateSpy).toHaveBeenNthCalledWith(2, '/settings/infrastructure/proxmox'); + }); + it('returns to the base settings route when switching away from direct proxmox', () => { mockPathname = '/settings/infrastructure/proxmox'; renderWorkspace(); - fireEvent.click(screen.getByRole('tab', { name: 'Reporting & control' })); + fireEvent.click(screen.getByRole('tab', { name: 'Install on a host' })); - expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations'); + expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install'); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 726e5bac2b..1d891af7d7 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -480,8 +480,12 @@ describe('Settings architecture guardrails', () => { expect(settingsInfrastructurePanelPropsSource).toContain('getInfrastructurePanelProps'); expect(settingsNavigationModelSource).toContain('export type SettingsTab ='); expect(settingsNavigationModelSource).toContain('export const DEFAULT_SETTINGS_TAB'); + expect(settingsNavigationModelSource).toContain( + "const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';", + ); expect(settingsNavigationModelSource).toContain('export function resolveCanonicalSettingsPath'); expect(settingsNavigationModelSource).toContain('export function settingsTabPath'); + expect(settingsNavigationModelSource).toContain("return INFRASTRUCTURE_INSTALL_PREFIX;"); expect(settingsNavigationHookSource).toContain('deriveTabFromPath'); expect(settingsNavigationHookSource).toContain('resolveCanonicalSettingsPath'); expect(settingsNavigationHookSource).toContain('settingsTabPath'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx index 7923abff0c..56017b9b21 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -7,7 +7,7 @@ import { updateDisableLocalUpgradeMetricsSetting } from '@/stores/systemSettings const canonicalTabPaths = { proxmox: '/settings/infrastructure/proxmox', - 'infrastructure-operations': '/settings/infrastructure/operations', + 'infrastructure-operations': '/settings/infrastructure/install', 'system-general': '/settings/system-general', 'system-network': '/settings/system-network', 'system-updates': '/settings/system-updates', diff --git a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts index d060da1157..93e04942db 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts @@ -17,7 +17,7 @@ import { isFeatureLocked, isTabLocked } from '../settingsFeatureGates'; const canonicalTabPaths = { proxmox: '/settings/infrastructure/proxmox', - 'infrastructure-operations': '/settings/infrastructure/operations', + 'infrastructure-operations': '/settings/infrastructure/install', 'system-general': '/settings/system-general', 'system-network': '/settings/system-network', 'system-updates': '/settings/system-updates', @@ -61,18 +61,18 @@ describe('settingsNavigationModel', () => { }); it('resolves only canonical settings paths', () => { - expect(resolveCanonicalSettingsPath('/settings')).toBe('/settings/infrastructure/operations'); + expect(resolveCanonicalSettingsPath('/settings')).toBe('/settings/infrastructure/install'); expect(resolveCanonicalSettingsPath('/settings/workloads')).toBe( - '/settings/infrastructure/operations', + '/settings/infrastructure/install', ); expect(resolveCanonicalSettingsPath('/settings/workloads/docker')).toBe( - '/settings/infrastructure/operations', + '/settings/infrastructure/install', ); expect(resolveCanonicalSettingsPath('/settings/system-updates')).toBe( '/settings/system-updates', ); expect(resolveCanonicalSettingsPath('/settings/infrastructure')).toBe( - '/settings/infrastructure/operations', + '/settings/infrastructure/install', ); expect(resolveCanonicalSettingsPath('/settings/infrastructure/pve')).toBe( '/settings/infrastructure/proxmox/pve', diff --git a/frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts b/frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts index 5b2d3774b3..5da9ac84ad 100644 --- a/frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts +++ b/frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts @@ -30,10 +30,13 @@ export function getInfrastructureWorkspaceViewFromPath( if (pathname.startsWith('/settings/infrastructure/proxmox')) { return 'direct'; } + if (pathname.startsWith('/settings/infrastructure/operations')) { + return 'inventory'; + } if (pathname.startsWith('/settings/infrastructure/install')) { return 'install'; } - return 'inventory'; + return 'install'; } export function buildInfrastructureWorkspacePath( @@ -41,6 +44,6 @@ export function buildInfrastructureWorkspacePath( ): string { return ( INFRASTRUCTURE_WORKSPACE_TABS.find((tab) => tab.id === view)?.path ?? - '/settings/infrastructure/operations' + '/settings/infrastructure/install' ); } diff --git a/frontend-modern/src/components/Settings/settingsNavigationModel.ts b/frontend-modern/src/components/Settings/settingsNavigationModel.ts index e0e6fb850e..201e3068f3 100644 --- a/frontend-modern/src/components/Settings/settingsNavigationModel.ts +++ b/frontend-modern/src/components/Settings/settingsNavigationModel.ts @@ -309,7 +309,7 @@ export function settingsTabPath(tab: SettingsTab): string { case 'proxmox': return PROXMOX_PREFIX; case 'infrastructure-operations': - return INFRASTRUCTURE_OPERATIONS_PREFIX; + return INFRASTRUCTURE_INSTALL_PREFIX; case 'system-recovery': return '/settings/system-recovery'; case 'organization-overview':