Align test suite with single-route infrastructure architecture

Updates all test files broken by the collapse of infrastructure sub-routes
(/install, /platforms, /connections) into one /settings/infrastructure entry.
Key contract deltas:

- settingsNavCatalog/Model: infrastructure-connections and infrastructure-install
  tabs removed; infrastructure-systems is the single sidebar entry
- infrastructureWorkspaceModel: getInfrastructureWorkspaceViewFromPath renamed
  to deriveAddStepFromLegacyPath; buildInfrastructureWorkspacePath now always
  returns /settings/infrastructure regardless of argument
- settingsNavigationModel: deriveAgentFromPath and isProxmoxSettingsPath use
  normalised (not canonical) path so Proxmox deep links still resolve to the
  correct agent before the canonical-path redirect fires
- useSettingsNavigation: Proxmox agent sync moved before the redirect early-
  return so deep links seed the correct selectedAgent
- InfrastructureInstallerSection: "Open Platform connections" button replaced
  with informational text; navigate target updated to flat workspace path
- useInfrastructureDiscoveryRuntimeState: currentTab parameter removed (tab
  collapse means discovery is always active when the workspace is mounted)
- SetupCompletionPanel: unified INFRASTRUCTURE_PATH constant replaces the two
  distinct INFRASTRUCTURE_INSTALL_PATH / INFRASTRUCTURE_PLATFORMS_PATH
- All navigation call sites (Dashboard, DashboardStateCards, Infrastructure
  empty-state) updated to /settings/infrastructure
- Contract updates: agent-lifecycle, api-contracts, frontend-primitives
This commit is contained in:
rcourtman 2026-04-19 00:29:38 +01:00
parent 9bac3f421d
commit fa2768ae78
32 changed files with 532 additions and 578 deletions

View file

@ -451,6 +451,12 @@ an add-only capacity posture.
## Current State
The infrastructure workspace collapsed to a single `/settings/infrastructure`
route. `buildInfrastructureWorkspacePath()` always returns the base path;
`deriveAddStepFromLegacyPath()` maps legacy sub-paths to in-page panel state.
`SetupCompletionPanel.tsx` uses one `INFRASTRUCTURE_PATH` constant for all
install and platform CTAs.
This subsystem now sits under the dedicated agent lifecycle and fleet
operations lane so install, registration, update continuity, profile
management, and fleet safety stop hiding inside architecture, migration, or
@ -2185,3 +2191,21 @@ test hooks, command-client factories, and timing overrides on the per-config or
per-agent instance instead of package-global variables, so concurrent
lifecycle-owned update and registration paths cannot leak one test or runtime
override into another agent session.
The infrastructure workspace now uses a single flat route
(`/settings/infrastructure`) instead of the former three-sub-route layout
(`/install`, `/platforms`, `/connections`). Panel state — which of the picker,
agent-install, or platform-specific screens is open — is managed in-page
through `InfrastructurePanelStep` signals rather than URL segments.
`frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
exposes `buildInfrastructureWorkspacePath()` (always returns the base path
regardless of argument) and `deriveAddStepFromLegacyPath()` (maps legacy deep
URLs to panel state for backwards-compatible deep-link resolution) as the sole
path-building and path-reading contract for lifecycle-adjacent install and
setup surfaces. Callers that formerly passed `'platforms'` or `'install'` to
`buildInfrastructureWorkspacePath` must use the no-argument form; the argument
is accepted but ignored so link-site callers can be updated incrementally
without breaking navigation.
`frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx` now uses
a single `INFRASTRUCTURE_PATH` constant (`/settings/infrastructure`) for all
install and platform-connection CTAs, replacing the former pair of
`INFRASTRUCTURE_INSTALL_PATH` and `INFRASTRUCTURE_PLATFORMS_PATH` constants.

View file

@ -526,6 +526,11 @@ the canonical monitored-system blocked payload.
## Current State
`useInfrastructureDiscoveryRuntimeState.ts` no longer gates `/api/discover`
polling on a settings tab name; polling is mount-scoped. The tab guard was
removed when the infrastructure nav collapsed to one `infrastructure-systems`
entry.
The API layer already uses contract tests in many places, but every major live
contract should continue moving toward canonical-only runtime shapes.
That same shared settings/licensing contract now also owns the split usage-data
@ -2893,3 +2898,12 @@ payloads without changing the runtime truth. Patrol transport may omit
only after canonical IDs have been resolved or a safe row-name fallback has
been chosen. API-adjacent browser callers must not reinterpret missing IDs or
preview arrays as authoritative empty success.
That same shared browser transport contract now also owns the discovery polling
mount scope.
`frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`
no longer gates `/api/discover` polling on the settings tab name; polling
starts whenever the hook is mounted and stops on cleanup. Callers must not
re-introduce a per-tab gate on this boundary. The discovery subnet settings
write path through `SettingsAPI.updateSystemSettings` remains governed by the
shared `internal/api/` settings boundary and is unaffected by the polling
scope change.

View file

@ -572,6 +572,12 @@ connections` visible as the API-backed alternative for Proxmox and
## Current State
`SettingsTab` no longer includes `infrastructure-connections` or
`infrastructure-install`. The single `infrastructure-systems` entry in
`settingsNavCatalog.ts`, `settingsPanelRegistry.ts`, and
`settingsNavigationModel.ts` replaces both. Panel routing within the
infrastructure area uses `InfrastructurePanelStep` in-page state.
The frontend already has several guardrail tests. The next step is to keep
turning repeated local patterns into explicit shared primitives with hard usage
bounds, including provider-backed alert-history wording. `frontend-modern/src/features/alerts/helpers.ts`,
@ -2238,3 +2244,18 @@ shared buttons must preserve discriminated disclosure props, toggle and a11y
helpers must expose exact event signatures, shared rows must accept typed
`data-*` props, and reporting-panel helpers must remain ES2020-safe instead of
depending on feature-local casts or newer string helpers.
The settings navigation model now exposes a single `infrastructure-systems`
sidebar entry for the infrastructure settings area. The former
`infrastructure-connections` and `infrastructure-install` entries have been
removed from `SettingsTab`, `settingsNavCatalog.ts`, `settingsPanelRegistry.ts`,
and `settingsNavigationModel.ts`. All canonical redirects and tab-derivation
logic that previously mapped to those two entries now collapse to
`infrastructure-systems`. No future additions to the settings nav may restore
`infrastructure-connections` or `infrastructure-install` as independent tab
identifiers; panel routing within the infrastructure area must use
`InfrastructurePanelStep` in-page state instead of URL sub-routes.
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` now uses
the normalised (not canonical) path when resolving Proxmox agent and path
checks so that deep links such as `/settings/infrastructure/platforms/proxmox/pbs`
resolve to the correct agent before the canonical-redirect fires, rather than
after it has already collapsed the path.

View file

@ -47,6 +47,6 @@ describe('DashboardStateCards', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open infrastructure setup' }));
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure/install');
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure');
});
});

View file

@ -465,15 +465,8 @@ export const InfrastructureInstallerSection: Component = () => {
{state.getSelectedInstallProfile().description}
</p>
<p class="mt-1.5 text-xs text-muted">
API-backed platforms such as TrueNAS connect through Platform connections
rather than a dedicated install profile here.{' '}
<button
type="button"
onClick={state.openPlatformConnections}
class="text-blue-600 underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Open Platform connections
</button>
API-backed platforms such as TrueNAS connect through Add infrastructure →
choose the platform type.
</p>
<Show when={state.getInstallProfileFlags().length > 0}>
<p class="mt-1.5 text-xs text-muted">

View file

@ -14,12 +14,17 @@ import { InfrastructureActiveRowDetails } from './InfrastructureActiveRowDetails
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
import { InfrastructureIgnoredRowDetails } from './InfrastructureIgnoredRowDetails';
import { InfrastructureStopMonitoringDialog } from './InfrastructureStopMonitoringDialog';
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
import { ProxmoxSettingsPanel } from './ProxmoxSettingsPanel';
import { TrueNASSettingsPanel } from './TrueNASSettingsPanel';
import { VMwareSettingsPanel } from './VMwareSettingsPanel';
import {
buildInfrastructureWorkspacePath,
getInfrastructureWorkspaceViewFromPath,
deriveAddStepFromLegacyPath,
type InfrastructureAddStep,
type InfrastructurePanelStep,
} from './infrastructureWorkspaceModel';
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
import type { NodeType } from './infrastructureSettingsModel';
import {
InfrastructureOperationsStateProvider,
useInfrastructureOperationsContext,
@ -27,17 +32,49 @@ import {
export type InfrastructureWorkspaceProps = InfrastructurePlatformSettingsProps;
const proxmoxRouteForKind = (kind: 'pve' | 'pbs' | 'pmg') =>
`/settings/infrastructure/platforms/proxmox/${kind}`;
const STEP_TITLES: Record<InfrastructureAddStep, string> = {
agent: 'Install on a host',
pve: 'Proxmox VE',
pbs: 'Proxmox Backup Server',
pmg: 'Proxmox Mail Gateway',
truenas: 'TrueNAS SCALE',
vmware: 'VMware vSphere / ESXi',
};
const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> = (props) => {
const navigate = useNavigate();
const location = useLocation();
const state = useInfrastructureOperationsContext();
const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname));
const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly());
const [showInstallProfiles, setShowInstallProfiles] = createSignal(false);
const [panelStep, setPanelStep] = createSignal<InfrastructurePanelStep | null>(null);
const [showAgentProfiles, setShowAgentProfiles] = createSignal(false);
const readOnly = createMemo(() => presentationPolicyIsReadOnly());
// Redirect legacy deep links and open the appropriate panel.
createEffect(() => {
const path = location.pathname;
if (path === '/settings/infrastructure') return;
const step = deriveAddStepFromLegacyPath(path);
navigate(buildInfrastructureWorkspacePath(), { replace: true });
if (step && !readOnly()) {
setPanelStep(step);
}
});
// Auto-open the agent installer when a setup handoff is waiting.
createEffect(() => {
if (state.setupHandoff?.() && panelStep() === null && !readOnly()) {
setPanelStep('agent');
}
});
// Close add panel in read-only mode.
createEffect(() => {
if (readOnly() && panelStep() !== null) {
setPanelStep(null);
}
});
const rows = createMemo<InfrastructureSystemRow[]>(() =>
buildInfrastructureSystemRows({
@ -45,52 +82,21 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
monitoringStoppedRows: state.monitoringStoppedRows(),
}),
);
const headerActions = createMemo<ConnectionsTableHeaderAction[]>(() =>
readOnlyWorkspace()
readOnly()
? []
: [
{
label: 'Add infrastructure',
onSelect: () => navigate(buildInfrastructureWorkspacePath('install'), { scroll: false }),
onSelect: () => {
setPanelStep('pick');
setShowAgentProfiles(false);
},
tone: 'primary' as const,
},
],
);
const closeConnectionsWorkspace = () => {
props.trueNASSettings.closeDialog?.();
props.trueNASSettings.closeDeleteDialog?.();
props.vmwareSettings.closeDialog?.();
props.vmwareSettings.closeDeleteDialog?.();
props.setShowNodeModal(false);
props.setEditingNode(null);
props.cancelDeleteNode?.();
navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false });
};
const closeInstallWorkspace = () => {
setShowInstallProfiles(false);
navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false });
};
const handleAddSystem = (choice: AddSystemChoice) => {
setShowInstallProfiles(false);
if (choice.kind === 'agent') {
navigate(buildInfrastructureWorkspacePath('install'), { scroll: false });
return;
}
if (choice.kind === 'truenas') {
navigate('/settings/infrastructure/platforms/truenas', { scroll: false });
return;
}
if (choice.kind === 'vmware') {
navigate('/settings/infrastructure/platforms/vmware', { scroll: false });
return;
}
navigate(proxmoxRouteForKind(choice.kind), { scroll: false });
};
const handleManageAction = (action: SystemManageAction) => {
switch (action.kind) {
@ -107,137 +113,191 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
}
};
createEffect(() => {
if (readOnlyWorkspace() && activeView() !== 'inventory') {
navigate(buildInfrastructureWorkspacePath('inventory'), { replace: true });
}
const handleAddSystem = (choice: AddSystemChoice) => {
setShowAgentProfiles(false);
setPanelStep(choice.kind as InfrastructureAddStep);
};
const closePanel = () => {
setPanelStep(null);
setShowAgentProfiles(false);
};
const goBackToPick = () => {
setShowAgentProfiles(false);
setPanelStep('pick');
};
// For Proxmox: selectedAgent is derived from the active panel step.
const proxmoxAgent = createMemo<NodeType>(() => {
const step = panelStep();
if (step === 'pbs') return 'pbs';
if (step === 'pmg') return 'pmg';
return 'pve';
});
createEffect(() => {
if (activeView() === 'inventory') {
return;
}
if (activeView() !== 'install') {
setShowInstallProfiles(false);
}
state.setExpandedRowKey(null);
state.setSelectedIgnoredRowKey(null);
});
const onSelectProxmoxAgent = (agent: NodeType) => {
setPanelStep(agent as InfrastructureAddStep);
};
const isProxmoxStep = () => {
const step = panelStep();
return step === 'pve' || step === 'pbs' || step === 'pmg';
};
const stepTitle = () => {
const step = panelStep();
if (!step || step === 'pick') return 'Add infrastructure';
return STEP_TITLES[step];
};
return (
<div class="space-y-8">
<Show when={activeView() === 'inventory'}>
<ConnectionsTable
rows={rows}
headerActions={headerActions()}
onManageRow={(row) => handleManageAction(row.manage)}
/>
</Show>
<ConnectionsTable
rows={rows}
headerActions={headerActions()}
onManageRow={(row) => handleManageAction(row.manage)}
/>
<Show when={!readOnlyWorkspace() && activeView() === 'install'}>
<div class="space-y-6">
<div class="space-y-3">
<button
type="button"
onClick={closeInstallWorkspace}
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Back to monitored systems
</button>
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Add infrastructure
</div>
<div class="text-xl font-semibold text-base-content">Choose what to connect</div>
<div class="text-sm text-muted">
Start with a host install or jump straight to an API-backed platform connection.
</div>
</div>
{/* Add-infrastructure panel — right drawer */}
<Dialog
isOpen={panelStep() !== null}
onClose={closePanel}
layout="drawer-right"
panelClass="max-w-[820px]"
ariaLabel={stepTitle()}
>
<div class="flex h-full flex-col overflow-hidden">
{/* Drawer header */}
<div class="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
<div class="flex items-center gap-2">
<Show when={panelStep() !== 'pick'}>
<button
type="button"
onClick={goBackToPick}
class="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
← Choose different type
</button>
</Show>
<span class="text-sm font-semibold text-base-content">{stepTitle()}</span>
</div>
<div class="flex items-center gap-2">
<Show when={panelStep() === 'agent'}>
<button
type="button"
onClick={() => setShowAgentProfiles((v) => !v)}
class="inline-flex items-center rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{showAgentProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
</button>
</Show>
<button
type="button"
onClick={() => setShowInstallProfiles((value) => !value)}
class="inline-flex items-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
onClick={closePanel}
class="rounded-md p-1 text-muted hover:bg-surface-hover hover:text-base-content"
aria-label="Close"
>
{showInstallProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<For each={ADD_SYSTEM_CHOICES}>
{(choice) => (
<button
type="button"
onClick={() => handleAddSystem(choice)}
class={`flex h-full flex-col items-start justify-between gap-4 rounded-xl border px-4 py-4 text-left transition-colors ${
choice.kind === 'agent'
? 'border-blue-300 bg-blue-50/60 text-base-content dark:border-blue-700 dark:bg-blue-950/20'
: 'border-border bg-surface hover:bg-surface-hover'
}`}
>
<div class="space-y-1.5">
<div class="text-sm font-semibold text-base-content">{choice.title}</div>
<div class="text-xs text-muted">{choice.description}</div>
{/* Drawer content */}
<div class="flex-1 overflow-y-auto">
{/* Step 1: picker */}
<Show when={panelStep() === 'pick'}>
<div class="space-y-4 p-4">
<p class="text-sm text-muted">
Choose the system or platform you want Pulse to start monitoring.
</p>
<ul class="divide-y divide-border rounded-md border border-border">
<For each={ADD_SYSTEM_CHOICES}>
{(choice) => (
<li>
<button
type="button"
onClick={() => handleAddSystem(choice)}
class="flex w-full items-start justify-between gap-4 px-4 py-3 text-left hover:bg-surface-hover"
>
<div>
<div class="text-sm font-semibold text-base-content">
{choice.title}
</div>
<div class="mt-0.5 text-xs text-muted">{choice.description}</div>
</div>
<span
class={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
choice.method === 'api'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
}`}
>
{choice.methodLabel}
</span>
</button>
</li>
)}
</For>
</ul>
</div>
</Show>
{/* Step 2a: agent installer */}
<Show when={panelStep() === 'agent'}>
<div class="space-y-4 p-4">
<Show when={showAgentProfiles()}>
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm">
<div class="mb-4 space-y-1">
<div class="text-base font-semibold text-base-content">Agent profiles</div>
<div class="text-sm text-muted">
Manage reusable install defaults for agent-based systems.
</div>
</div>
<AgentProfilesPanel />
</div>
<span
class={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
choice.method === 'api'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
}`}
>
{choice.methodLabel}
</span>
</button>
)}
</For>
</div>
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
<InfrastructureInstallerSection />
</div>
<Show when={showInstallProfiles()}>
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
<div class="mb-4 space-y-1">
<div class="text-lg font-semibold text-base-content">Agent profiles</div>
<div class="text-sm text-muted">
Manage reusable install defaults for agent-based systems.
</div>
</Show>
<InfrastructureInstallerSection />
</div>
<AgentProfilesPanel />
</div>
</Show>
</Show>
{/* Step 2b: Proxmox (PVE / PBS / PMG) */}
<Show when={isProxmoxStep()}>
<div class="p-4">
<ProxmoxSettingsPanel
{...props}
selectedAgent={proxmoxAgent}
onSelectAgent={onSelectProxmoxAgent}
embedded
/>
</div>
</Show>
{/* Step 2c: TrueNAS */}
<Show when={panelStep() === 'truenas'}>
<div class="p-4">
<TrueNASSettingsPanel state={props.trueNASSettings} />
</div>
</Show>
{/* Step 2d: VMware */}
<Show when={panelStep() === 'vmware'}>
<div class="p-4">
<VMwareSettingsPanel state={props.vmwareSettings} />
</div>
</Show>
</div>
</div>
</Show>
<Show when={!readOnlyWorkspace() && activeView() === 'platforms'}>
<div class="space-y-6">
<div class="space-y-3">
<button
type="button"
onClick={closeConnectionsWorkspace}
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Back to monitored systems
</button>
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Connections
</div>
<div class="text-xl font-semibold text-base-content">Platform connections</div>
<div class="text-sm text-muted">
Configure API-backed providers in a full workspace instead of a modal overlay.
</div>
</div>
</div>
<PlatformConnectionsWorkspace {...props} />
</div>
</Show>
<InfrastructureStopMonitoringDialog />
</Dialog>
{/* Active system detail drawer */}
<Dialog
isOpen={Boolean(state.selectedActiveRow())}
onClose={() => state.setExpandedRowKey(null)}
@ -250,6 +310,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
</Show>
</Dialog>
{/* Ignored system detail drawer */}
<Dialog
isOpen={Boolean(state.selectedIgnoredRow())}
onClose={() => state.setSelectedIgnoredRowKey(null)}
@ -261,6 +322,8 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
{(rowAccessor) => <InfrastructureIgnoredRowDetails rowAccessor={rowAccessor} />}
</Show>
</Dialog>
<InfrastructureStopMonitoringDialog />
</div>
);
};

View file

@ -34,7 +34,7 @@ const Settings: Component<SettingsProps> = (props) => {
const { state, connected: _connected } = useWebSocket();
const navigate = useNavigate();
const location = useLocation();
const { currentTab, activeTab, selectedAgent, setActiveTab, handleSelectAgent } =
const { activeTab, selectedAgent, setActiveTab, handleSelectAgent } =
useSettingsNavigation({
navigate,
location,
@ -74,7 +74,6 @@ const Settings: Component<SettingsProps> = (props) => {
});
const infrastructureSettings = useInfrastructureSettingsState({
eventBus,
currentTab,
discoveryEnabled: discoverySettings.discoveryEnabled,
setDiscoveryEnabled: discoverySettings.setDiscoveryEnabled,
discoverySubnet: discoverySettings.discoverySubnet,

View file

@ -111,7 +111,7 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
expect(infrastructureInstallStateSource).toContain('MonitoringAPI.getState()');
expect(infrastructureInstallStateSource).toContain('./infrastructureWorkspaceModel');
expect(infrastructureInstallStateSource).toContain(
"navigate(buildInfrastructureWorkspacePath('platforms'))",
"navigate(buildInfrastructureWorkspacePath())",
);
expect(infrastructureInstallStateSource).not.toContain(
"navigate('/settings/infrastructure/proxmox')",
@ -130,7 +130,7 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
);
expect(infrastructureDiscoveryRuntimeStateSource).toContain("apiFetch('/api/discover'");
expect(infrastructureDiscoveryRuntimeStateSource).toContain(
"currentTab() === 'infrastructure-connections'",
"discoveryEnabled: Accessor<boolean>",
);
expect(infrastructureDiscoveryRuntimeStateSource).toContain('SettingsAPI.updateSystemSettings');
expect(infrastructureInstallerSectionSource).toContain('useInfrastructureOperationsContext');
@ -2725,9 +2725,9 @@ describe('InfrastructureOperationsController platform commands', () => {
expect(screen.queryByRole('option', { name: 'TrueNAS SCALE agent' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: /Open Platform connections/i }));
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/platforms');
expect(
screen.getByText(/API-backed platforms such as TrueNAS connect through Add infrastructure/i),
).toBeInTheDocument();
});
it('tracks install command copies', async () => {

View file

@ -5,6 +5,7 @@ import type { UnifiedAgentRow } from '../infrastructureOperationsModel';
import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
let mockPathname = '/settings/infrastructure';
let mockSearch = '';
const navigateSpy = vi.hoisted(() => vi.fn());
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
const setExpandedRowKeySpy = vi.hoisted(() => vi.fn());
@ -19,7 +20,7 @@ vi.mock('@solidjs/router', async () => {
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
return {
...actual,
useLocation: () => ({ pathname: mockPathname }),
useLocation: () => ({ pathname: mockPathname, search: mockSearch }),
useNavigate: () => navigateSpy,
};
});
@ -45,6 +46,7 @@ vi.mock('../useInfrastructureOperationsState', () => ({
setSelectedIgnoredRowKeySpy(value);
setSelectedIgnoredRowKey(value);
},
setupHandoff: () => null,
}),
}));
@ -52,8 +54,16 @@ vi.mock('../InfrastructureInstallerSection', () => ({
InfrastructureInstallerSection: () => <div data-testid="install-section">install</div>,
}));
vi.mock('../PlatformConnectionsWorkspace', () => ({
PlatformConnectionsWorkspace: () => <div data-testid="platform-section">platforms</div>,
vi.mock('../ProxmoxSettingsPanel', () => ({
ProxmoxSettingsPanel: () => <div data-testid="proxmox-section">proxmox</div>,
}));
vi.mock('../TrueNASSettingsPanel', () => ({
TrueNASSettingsPanel: () => <div data-testid="truenas-section">truenas</div>,
}));
vi.mock('../VMwareSettingsPanel', () => ({
VMwareSettingsPanel: () => <div data-testid="vmware-section">vmware</div>,
}));
vi.mock('../InfrastructureActiveRowDetails', () => ({
@ -144,6 +154,7 @@ describe('InfrastructureWorkspace', () => {
setCurrentNodeTypeSpy.mockReset();
setModalResetKeySpy.mockReset();
mockPathname = '/settings/infrastructure';
mockSearch = '';
mockActiveRows = [reportingRow()];
mockIgnoredRows = [];
setSelectedActiveRowKey(null);
@ -157,19 +168,17 @@ describe('InfrastructureWorkspace', () => {
const renderWorkspace = (propOverrides: Record<string, unknown> = {}) =>
render(() => (<InfrastructureWorkspace {...{ ...baseProps(), ...propOverrides }} />) as any);
it('renders only the top-level ledger at the base infrastructure route', () => {
it('renders the inventory table at the base infrastructure route', () => {
renderWorkspace();
expect(screen.getByRole('heading', { name: 'Monitored systems' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Connections' })).toBeNull();
expect(screen.queryByRole('button', { name: 'Agent profiles' })).toBeNull();
expect(screen.queryByTestId('platform-section')).toBeNull();
expect(screen.queryByTestId('install-section')).toBeNull();
expect(screen.queryByTestId('proxmox-section')).toBeNull();
expect(screen.queryByTestId('agent-profiles')).toBeNull();
});
it('shows monitored systems only in the top ledger', () => {
it('shows only monitored agents in the inventory table', () => {
renderWorkspace();
expect(screen.getByText('tower')).toBeInTheDocument();
@ -178,17 +187,90 @@ describe('InfrastructureWorkspace', () => {
expect(screen.queryByText('lab-vcenter')).toBeNull();
});
it('routes the add-infrastructure action to the install workspace', () => {
it('opens the type picker in the add panel when Add infrastructure is clicked', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install', {
scroll: false,
});
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Install on a host/i })).toBeInTheDocument();
expect(navigateSpy).not.toHaveBeenCalled();
});
it('opens reporting details from the top ledger in a drawer', () => {
it('shows the agent installer when Install on a host is chosen', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
expect(screen.getByTestId('install-section')).toBeInTheDocument();
expect(screen.queryByText('What do you want to connect?')).toBeNull();
});
it('shows the TrueNAS panel when TrueNAS SCALE is chosen', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i }));
expect(screen.getByTestId('truenas-section')).toBeInTheDocument();
expect(navigateSpy).not.toHaveBeenCalled();
});
it('shows the VMware panel when VMware vSphere is chosen', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i }));
expect(screen.getByTestId('vmware-section')).toBeInTheDocument();
expect(navigateSpy).not.toHaveBeenCalled();
});
it('shows the Proxmox panel when Proxmox VE is chosen', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
expect(screen.getByTestId('proxmox-section')).toBeInTheDocument();
expect(navigateSpy).not.toHaveBeenCalled();
});
it('returns to the picker via back button from a step-2 form', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
fireEvent.click(screen.getByRole('button', { name: /Choose different type/i }));
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
expect(screen.queryByTestId('install-section')).toBeNull();
});
it('toggles agent profiles in the agent install drawer', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
});
it('does not use modals or child-node setup, only sets panel state on no-modal choices', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
expect(setShowNodeModalSpy).not.toHaveBeenCalled();
expect(setEditingNodeSpy).not.toHaveBeenCalled();
expect(setCurrentNodeTypeSpy).not.toHaveBeenCalled();
expect(setModalResetKeySpy).not.toHaveBeenCalled();
});
it('opens reporting details from the inventory in a drawer', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: 'View details' }));
@ -197,82 +279,36 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByTestId('active-details')).toBeInTheDocument();
});
it('renders platform connections as a full-page workspace on platform deep links', () => {
it('redirects legacy install deep link to the base route and opens the agent panel', () => {
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
expect(screen.getByTestId('install-section')).toBeInTheDocument();
});
it('redirects legacy platforms deep link to the base route and opens the picker', () => {
mockPathname = '/settings/infrastructure/platforms/truenas';
renderWorkspace();
expect(screen.queryByRole('heading', { name: 'Monitored systems' })).toBeNull();
expect(screen.getByText('Platform connections')).toBeInTheDocument();
expect(screen.getByTestId('platform-section')).toBeInTheDocument();
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
expect(screen.getByTestId('truenas-section')).toBeInTheDocument();
});
it('hides Add infrastructure and the add panel in read-only mode', () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
renderWorkspace();
expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull();
expect(screen.queryByTestId('install-section')).toBeNull();
});
it('renders install tools as a full-page workspace on install deep links', () => {
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
expect(screen.getByText('Choose what to connect')).toBeInTheDocument();
expect(screen.getByText('Install on a host')).toBeInTheDocument();
expect(screen.getByTestId('install-section')).toBeInTheDocument();
expect(screen.queryByTestId('platform-section')).toBeNull();
});
it('opens agent profiles inline inside the install workspace', () => {
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
});
it('returns from platform connections to the monitored systems route', () => {
mockPathname = '/settings/infrastructure/platforms/truenas';
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: 'Back to monitored systems' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
scroll: false,
});
});
it('routes infrastructure choices through full-page setup routes instead of auto-opening dialogs', () => {
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas', {
scroll: false,
});
fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware', {
scroll: false,
});
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pve', {
scroll: false,
});
expect(setShowNodeModalSpy).not.toHaveBeenCalled();
expect(setEditingNodeSpy).not.toHaveBeenCalled();
expect(setCurrentNodeTypeSpy).not.toHaveBeenCalled();
expect(setModalResetKeySpy).not.toHaveBeenCalled();
});
it('collapses read-only sessions back to inventory and hides setup sections', () => {
it('collapses read-only sessions back to the inventory and redirects', () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull();
expect(screen.queryByRole('button', { name: 'Connections' })).toBeNull();
expect(screen.queryByRole('button', { name: 'Agent profiles' })).toBeNull();
expect(screen.queryByTestId('platform-section')).toBeNull();
expect(screen.queryByTestId('install-section')).toBeNull();
expect(screen.queryByTestId('agent-profiles')).toBeNull();
});
});

View file

@ -779,7 +779,7 @@ describe('monitored-system model guardrails', () => {
"navigate('/settings/infrastructure/proxmox')",
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function getInfrastructureWorkspaceViewFromPath',
'export function deriveAddStepFromLegacyPath',
);
expect(infrastructureWorkspaceModelSource).toContain(
"pathname.startsWith('/settings/infrastructure/proxmox')",

View file

@ -510,9 +510,6 @@ 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 isProxmoxSettingsPath');
expect(settingsNavigationModelSource).toContain('export function settingsTabPath');
@ -525,7 +522,7 @@ describe('Settings architecture guardrails', () => {
expect(settingsNavigationHookSource).toContain('resolveCanonicalSelfHostedBillingHref');
expect(settingsNavigationHookSource).toContain('settingsTabPath');
expect(settingsNavigationHookSource).toContain('presentationPolicyIsReadOnly');
expect(settingsNavigationHookSource).toContain("buildInfrastructureWorkspacePath('inventory')");
expect(settingsNavigationHookSource).toContain('buildInfrastructureWorkspacePath');
expect(settingsNavigationModelSource).toContain('hideFromSidebar?: boolean;');
expect(settingsRoutingSource).toContain("from './settingsNavigationModel'");
expect(settingsTypesSource).toContain("from './settingsNavigationModel'");
@ -887,9 +884,7 @@ describe('Settings architecture guardrails', () => {
'createSignal<InfrastructureWorkspaceView>',
);
expect(infrastructureWorkspaceSource).toContain('createEffect(() =>');
expect(infrastructureWorkspaceSource).toContain(
"if (readOnlyWorkspace() && activeView() !== 'inventory')",
);
expect(infrastructureWorkspaceSource).toContain('readOnly()');
expect(infrastructureWorkspaceSource).toContain('./ConnectionsTable');
expect(infrastructureWorkspaceSource).toContain('./InfrastructureActiveRowDetails');
expect(infrastructureWorkspaceSource).toContain('./InfrastructureIgnoredRowDetails');
@ -897,15 +892,12 @@ describe('Settings architecture guardrails', () => {
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallerSection');
expect(infrastructureWorkspaceSource).toContain('InfrastructureStopMonitoringDialog');
expect(infrastructureWorkspaceSource).toContain('AgentProfilesPanel');
expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace');
expect(infrastructureWorkspaceSource).not.toContain(
"import { Subtabs } from '@/components/shared/Subtabs'",
);
expect(infrastructureWorkspaceSource).not.toContain('<Subtabs');
expect(infrastructureWorkspaceSource).toContain('Platform connections');
expect(infrastructureWorkspaceSource).toContain('Choose what to connect');
expect(infrastructureWorkspaceSource).toContain('activeView() === \'platforms\'');
expect(infrastructureWorkspaceSource).toContain('activeView() === \'install\'');
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'pick'");
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'agent'");
expect(infrastructureWorkspaceSource).not.toContain("activeView() === 'operations'");
expect(connectionsTableSource).toContain('headerActions');
expect(connectionsTableModelSource).toContain('export function buildInfrastructureSystemRows');
@ -964,7 +956,7 @@ describe('Settings architecture guardrails', () => {
'export const INFRASTRUCTURE_WORKSPACE_TABS',
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function getInfrastructureWorkspaceViewFromPath',
'export function deriveAddStepFromLegacyPath',
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function buildInfrastructureWorkspacePath',
@ -1438,8 +1430,6 @@ describe('Settings architecture guardrails', () => {
expect(SETTINGS_HEADER_META['infrastructure-systems'].description).toBe(
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
);
expect(SETTINGS_HEADER_META['infrastructure-connections'].title).toBe('Infrastructure');
expect(SETTINGS_HEADER_META['infrastructure-install'].title).toBe('Infrastructure');
});
it('keeps billing-related shell framing on self-hosted commercial terms', () => {
@ -1489,13 +1479,13 @@ describe('Settings architecture guardrails', () => {
it('keeps read-only infrastructure landing and shell copy on the shared settings owner', () => {
expect(settingsNavigationHookSource).toContain('presentationPolicyIsReadOnly');
expect(settingsNavigationHookSource).toContain("buildInfrastructureWorkspacePath('inventory')");
expect(settingsNavigationHookSource).toContain('buildInfrastructureWorkspacePath');
expect(settingsShellStateSource).toContain('presentationPolicyIsReadOnly');
expect(settingsShellStateSource).toContain(
'Setup changes stay unavailable in this read-only session.',
);
expect(infrastructureWorkspaceSource).toContain('presentationPolicyIsReadOnly');
expect(infrastructureWorkspaceSource).toContain("activeView() !== 'inventory'");
expect(infrastructureWorkspaceSource).toContain('readOnly()');
});
it('keeps relay shell copy on the shared relay presentation owner', () => {

View file

@ -7,8 +7,6 @@ import { updateDisableLocalUpgradeMetricsSetting } from '@/stores/systemSettings
const canonicalTabPaths = {
'infrastructure-systems': '/settings/infrastructure',
'infrastructure-connections': '/settings/infrastructure/platforms',
'infrastructure-install': '/settings/infrastructure/install',
'system-general': '/settings/system-general',
'system-network': '/settings/system-network',
'system-updates': '/settings/system-updates',
@ -309,7 +307,7 @@ describe('settingsNavigation integration scaffold', () => {
});
it('keeps non-gated tabs unlocked', () => {
expect(isTabLocked('infrastructure-connections', hasFeatures([]), () => true)).toBe(false);
expect(isTabLocked('infrastructure-systems', hasFeatures([]), () => true)).toBe(false);
});
it('getTabLockReason returns tier-specific reason for locked tabs and null for unlocked', () => {
@ -325,7 +323,7 @@ describe('settingsNavigation integration scaffold', () => {
);
expect(getTabLockReason(tab, hasFeatures([requiredFeature]), () => true)).toBeNull();
}
expect(getTabLockReason('infrastructure-connections', hasFeatures([]), () => true)).toBeNull();
expect(getTabLockReason('infrastructure-systems', hasFeatures([]), () => true)).toBeNull();
});
});

View file

@ -18,8 +18,6 @@ import { isFeatureLocked, isTabLocked } from '../settingsFeatureGates';
const canonicalTabPaths = {
'infrastructure-systems': '/settings/infrastructure',
'infrastructure-connections': '/settings/infrastructure/platforms',
'infrastructure-install': '/settings/infrastructure/install',
'system-general': '/settings/system-general',
'system-network': '/settings/system-network',
'system-updates': '/settings/system-updates',
@ -80,30 +78,29 @@ describe('settingsNavigationModel', () => {
expect(resolveCanonicalSettingsPath('/settings/infrastructure')).toBe(
'/settings/infrastructure',
);
// All infrastructure sub-paths collapse to the single workspace.
expect(resolveCanonicalSettingsPath('/settings/infrastructure/install')).toBe(
'/settings/infrastructure/install',
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/pve')).toBe(
'/settings/infrastructure/platforms/proxmox/pve',
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api')).toBe(
'/settings/infrastructure/platforms/proxmox',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api/pve')).toBe(
'/settings/infrastructure/platforms/proxmox/pve',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/proxmox')).toBe(
'/settings/infrastructure/platforms/proxmox',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/truenas')).toBe(
'/settings/infrastructure/platforms/truenas',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/vmware')).toBe(
'/settings/infrastructure/platforms/vmware',
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms/truenas')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/operations')).toBe(
'/settings/infrastructure',
);
// Legacy proxmox paths redirect to the workspace.
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api/pve')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/proxmox')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/integrations/api')).toBe(
'/settings/security/api',
);
@ -147,10 +144,10 @@ describe('settingsNavigationModel', () => {
['?tab=infrastructure', 'infrastructure-systems'],
['?tab=agents', 'infrastructure-systems'],
['?tab=workloads', 'infrastructure-systems'],
['?tab=proxmox', 'infrastructure-connections'],
['?tab=connections', 'infrastructure-connections'],
['?tab=platforms', 'infrastructure-connections'],
['?tab=install', 'infrastructure-install'],
['?tab=proxmox', 'infrastructure-systems'],
['?tab=connections', 'infrastructure-systems'],
['?tab=platforms', 'infrastructure-systems'],
['?tab=install', 'infrastructure-systems'],
['?tab=docker', 'infrastructure-systems'],
['?tab=system-ai', 'system-ai'],
['?tab=system-relay', 'system-relay'],
@ -195,14 +192,16 @@ describe('settingsNavigationModel', () => {
expect(isTabLocked(tab, hasFeatures([requiredFeature]), () => true)).toBe(false);
expect(isTabLocked(tab, hasFeatures([]), () => false)).toBe(false);
}
expect(isTabLocked('infrastructure-connections', hasFeatures([]), () => true)).toBe(false);
expect(isTabLocked('infrastructure-systems', hasFeatures([]), () => true)).toBe(false);
});
it('maps deriveAgentFromPath contracts for canonical infrastructure routes', () => {
const agentCases: Array<[string, 'pve' | 'pbs' | 'pmg' | null]> = [
['/settings/infrastructure/api/pve', 'pve'],
['/settings/infrastructure/api/pbs', 'pbs'],
['/settings/infrastructure/api/pmg', 'pmg'],
// Legacy api/* paths now resolve to the workspace base — no agent derived.
['/settings/infrastructure/api/pve', null],
['/settings/infrastructure/api/pbs', null],
['/settings/infrastructure/api/pmg', null],
// Canonical platform paths still resolve correctly.
['/settings/infrastructure/platforms/proxmox/pve', 'pve'],
['/settings/infrastructure/platforms/proxmox/pbs', 'pbs'],
['/settings/infrastructure/platforms/proxmox/pmg', 'pmg'],
@ -242,19 +241,15 @@ describe('settingsNavigationModel', () => {
});
it('treats proxmox deep links as infrastructure aliases', () => {
expect(deriveTabFromPath('/settings/infrastructure/platforms')).toBe(
'infrastructure-connections',
);
expect(deriveTabFromPath('/settings/infrastructure/platforms')).toBe('infrastructure-systems');
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox')).toBe(
'infrastructure-connections',
'infrastructure-systems',
);
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox/pve')).toBe(
'infrastructure-connections',
'infrastructure-systems',
);
expect(deriveTabFromPath('/settings/infrastructure/api/pve')).toBe(
'infrastructure-connections',
);
expect(deriveTabFromPath('/settings/infrastructure/install')).toBe('infrastructure-install');
expect(deriveTabFromPath('/settings/infrastructure/api/pve')).toBe('infrastructure-systems');
expect(deriveTabFromPath('/settings/infrastructure/install')).toBe('infrastructure-systems');
});
it('maps support and legacy operations routes back to support tabs', () => {

View file

@ -22,8 +22,8 @@ function renderHarness(pathname = '/settings', search = '', hash = '') {
return (
<>
<button type="button" onClick={() => navigation.setActiveTab('infrastructure-connections')}>
open connections settings
<button type="button" onClick={() => navigation.setActiveTab('infrastructure-systems')}>
open infrastructure settings
</button>
<div data-testid="selected-agent">{navigation.selectedAgent()}</div>
</>
@ -55,7 +55,7 @@ describe('useSettingsNavigation', () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
renderHarness('/settings/system-general');
fireEvent.click(screen.getByRole('button', { name: 'open connections settings' }));
fireEvent.click(screen.getByRole('button', { name: 'open infrastructure settings' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
scroll: false,

View file

@ -1,28 +1,24 @@
export type InfrastructureWorkspaceView = 'inventory' | 'install' | 'platforms';
export type InfrastructureAddStep = 'agent' | 'pve' | 'pbs' | 'pmg' | 'truenas' | 'vmware';
export type InfrastructurePanelStep = 'pick' | InfrastructureAddStep;
const INFRASTRUCTURE_WORKSPACE_PATHS: Record<InfrastructureWorkspaceView, string> = {
inventory: '/settings/infrastructure',
install: '/settings/infrastructure/install',
platforms: '/settings/infrastructure/platforms',
};
const INFRASTRUCTURE_BASE_PATH = '/settings/infrastructure';
export function getInfrastructureWorkspaceViewFromPath(
export function buildInfrastructureWorkspacePath(_view?: string): string {
return INFRASTRUCTURE_BASE_PATH;
}
export function deriveAddStepFromLegacyPath(
pathname: string,
): InfrastructureWorkspaceView {
if (
pathname.startsWith('/settings/infrastructure/platforms') ||
pathname.startsWith('/settings/infrastructure/proxmox') ||
pathname.startsWith('/settings/infrastructure/api') ||
pathname.startsWith('/settings/infrastructure/truenas')
) {
return 'platforms';
}
if (pathname.startsWith('/settings/infrastructure/install')) {
return 'install';
}
return 'inventory';
}
export function buildInfrastructureWorkspacePath(view: InfrastructureWorkspaceView): string {
return INFRASTRUCTURE_WORKSPACE_PATHS[view];
): InfrastructurePanelStep | null {
if (pathname.startsWith('/settings/infrastructure/install')) return 'agent';
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox/pbs')) return 'pbs';
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox/pmg')) return 'pmg';
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox')) return 'pve';
if (pathname.startsWith('/settings/infrastructure/platforms/truenas')) return 'truenas';
if (pathname.startsWith('/settings/infrastructure/platforms/vmware')) return 'vmware';
if (pathname.startsWith('/settings/infrastructure/platforms')) return 'pick';
if (pathname.startsWith('/settings/infrastructure/proxmox')) return 'pve';
if (pathname.startsWith('/settings/infrastructure/truenas')) return 'truenas';
if (pathname.startsWith('/settings/infrastructure/vmware')) return 'vmware';
return null;
}

View file

@ -9,18 +9,7 @@ import {
export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = {
'infrastructure-systems': {
title: 'Infrastructure',
description:
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
},
'infrastructure-connections': {
title: 'Infrastructure',
description:
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
},
'infrastructure-install': {
title: 'Infrastructure',
description:
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
description: `Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
},
'system-general': {
title: 'General',

View file

@ -6,8 +6,6 @@ import Key from 'lucide-solid/icons/key';
import Activity from 'lucide-solid/icons/activity';
import Network from 'lucide-solid/icons/network';
import Server from 'lucide-solid/icons/server';
import PlugZap from 'lucide-solid/icons/plug-zap';
import Download from 'lucide-solid/icons/download';
import Users from 'lucide-solid/icons/users';
import Sliders from 'lucide-solid/icons/sliders-horizontal';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
@ -39,22 +37,6 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [
icon: Server,
iconProps: { strokeWidth: 2 },
},
{
id: 'infrastructure-connections',
label: 'Connections',
icon: PlugZap,
iconProps: { strokeWidth: 2 },
hideFromSidebar: true,
hideWhenReadOnly: true,
},
{
id: 'infrastructure-install',
label: 'Install',
icon: Download,
iconProps: { strokeWidth: 2 },
hideFromSidebar: true,
hideWhenReadOnly: true,
},
],
},
{

View file

@ -9,8 +9,6 @@ import {
export type SettingsTab =
| 'infrastructure-systems'
| 'infrastructure-connections'
| 'infrastructure-install'
| 'system-general'
| 'system-network'
| 'system-updates'
@ -35,10 +33,7 @@ export type SettingsTab =
| 'security-audit'
| 'security-webhooks';
export type InfrastructureSettingsTab = Extract<
SettingsTab,
'infrastructure-systems' | 'infrastructure-connections' | 'infrastructure-install'
>;
export type InfrastructureSettingsTab = Extract<SettingsTab, 'infrastructure-systems'>;
export type AgentKey = 'pve' | 'pbs' | 'pmg';
export type ProxmoxPlatformType = Extract<
@ -92,19 +87,13 @@ export const DEFAULT_SETTINGS_TAB: SettingsTab = 'infrastructure-systems';
const INFRASTRUCTURE_SYSTEMS_PREFIX = '/settings/infrastructure';
const LEGACY_AGENTS_PREFIX = '/settings/workloads';
const LEGACY_DOCKER_PREFIX = '/settings/workloads/docker';
const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';
const INFRASTRUCTURE_OPERATIONS_PREFIX = '/settings/infrastructure/operations';
const PLATFORM_CONNECTIONS_PREFIX = '/settings/infrastructure/platforms';
const SUPPORT_PREFIX = '/settings/support';
const SUPPORT_DIAGNOSTICS_PREFIX = `${SUPPORT_PREFIX}/diagnostics`;
const SUPPORT_REPORTING_PREFIX = `${SUPPORT_PREFIX}/reporting`;
const SUPPORT_LOGS_PREFIX = `${SUPPORT_PREFIX}/logs`;
const TRUENAS_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/truenas`;
const PROXMOX_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/proxmox`;
const PROXMOX_PREFIX = `${INFRASTRUCTURE_SYSTEMS_PREFIX}/platforms/proxmox`;
const LEGACY_PROXMOX_PREFIX = '/settings/infrastructure/proxmox';
const LEGACY_PROXMOX_API_PREFIX = '/settings/infrastructure/api';
const LEGACY_TRUENAS_PREFIX = '/settings/infrastructure/truenas';
const LEGACY_VMWARE_PREFIX = '/settings/infrastructure/vmware';
const LEGACY_INTEGRATIONS_API_PREFIX = '/settings/integrations/api';
const LEGACY_SETTINGS_OPERATIONS_PREFIX = '/settings/operations';
const SECURITY_API_PREFIX = '/settings/security/api';
@ -158,19 +147,14 @@ export function resolveCanonicalSettingsPath(path: string): string | null {
if (normalizedPath === LEGACY_AGENTS_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
if (normalizedPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
if (normalizedPath === LEGACY_DOCKER_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
if (normalizedPath === PLATFORM_CONNECTIONS_PREFIX) {
return PLATFORM_CONNECTIONS_PREFIX;
}
if (normalizedPath === INFRASTRUCTURE_INSTALL_PREFIX) {
return INFRASTRUCTURE_INSTALL_PREFIX;
}
if (normalizedPath === INFRASTRUCTURE_OPERATIONS_PREFIX) {
// All infrastructure sub-paths collapse to the single infrastructure workspace.
if (normalizedPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`)) {
return INFRASTRUCTURE_SYSTEMS_PREFIX;
}
if (normalizedPath === LEGACY_DOCKER_PREFIX) {
if (normalizedPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
if (normalizedPath === SUPPORT_PREFIX) {
@ -182,44 +166,13 @@ export function resolveCanonicalSettingsPath(path: string): string | null {
if (normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/`)) {
return buildLegacyOperationsSettingsPath(normalizedPath);
}
if (normalizedPath === LEGACY_PROXMOX_API_PREFIX) {
return PROXMOX_PREFIX;
}
if (normalizedPath === LEGACY_PROXMOX_PREFIX) {
return PROXMOX_PREFIX;
}
if (normalizedPath === LEGACY_TRUENAS_PREFIX) {
return TRUENAS_PREFIX;
}
if (normalizedPath === LEGACY_VMWARE_PREFIX) {
return `${PLATFORM_CONNECTIONS_PREFIX}/vmware`;
}
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pve`) {
return `${PROXMOX_PREFIX}/pve`;
}
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pbs`) {
return `${PROXMOX_PREFIX}/pbs`;
}
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pmg`) {
return `${PROXMOX_PREFIX}/pmg`;
}
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pve`) {
return `${PROXMOX_PREFIX}/pve`;
}
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pbs`) {
return `${PROXMOX_PREFIX}/pbs`;
}
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pmg`) {
return `${PROXMOX_PREFIX}/pmg`;
}
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pve`) {
return `${PROXMOX_PREFIX}/pve`;
}
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pbs`) {
return `${PROXMOX_PREFIX}/pbs`;
}
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pmg`) {
return `${PROXMOX_PREFIX}/pmg`;
if (
normalizedPath.startsWith(`${LEGACY_PROXMOX_API_PREFIX}/`) ||
normalizedPath === LEGACY_PROXMOX_API_PREFIX ||
normalizedPath.startsWith(`${LEGACY_PROXMOX_PREFIX}/`) ||
normalizedPath === LEGACY_PROXMOX_PREFIX
) {
return INFRASTRUCTURE_SYSTEMS_PREFIX;
}
if (normalizedPath === LEGACY_INTEGRATIONS_API_PREFIX) {
return SECURITY_API_PREFIX;
@ -239,25 +192,12 @@ export function deriveTabFromPath(path: string): SettingsTab {
if (canonicalPath === '/settings') return DEFAULT_SETTINGS_TAB;
if (
canonicalPath === INFRASTRUCTURE_SYSTEMS_PREFIX ||
canonicalPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`) ||
canonicalPath === LEGACY_AGENTS_PREFIX ||
canonicalPath === LEGACY_DOCKER_PREFIX
) {
return 'infrastructure-systems';
}
if (canonicalPath === INFRASTRUCTURE_INSTALL_PREFIX) {
return 'infrastructure-install';
}
if (
canonicalPath === PLATFORM_CONNECTIONS_PREFIX ||
canonicalPath.startsWith(PROXMOX_PREFIX) ||
canonicalPath.startsWith(TRUENAS_PREFIX) ||
canonicalPath.startsWith(`${PLATFORM_CONNECTIONS_PREFIX}/vmware`) ||
canonicalPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
canonicalPath.startsWith(LEGACY_PROXMOX_API_PREFIX) ||
canonicalPath.startsWith(LEGACY_TRUENAS_PREFIX) ||
canonicalPath.startsWith(LEGACY_VMWARE_PREFIX)
)
return 'infrastructure-connections';
if (canonicalPath.includes('/settings/system-general')) return 'system-general';
if (canonicalPath.includes('/settings/system-network')) return 'system-network';
@ -293,23 +233,21 @@ export function deriveTabFromPath(path: string): SettingsTab {
}
export function deriveAgentFromPath(path: string): AgentKey | null {
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
const normalizedPath = normalizeSettingsPath(path);
for (const [agent, meta] of Object.entries(PROXMOX_AGENT_META) as Array<
[AgentKey, (typeof PROXMOX_AGENT_META)[AgentKey]]
>) {
if (canonicalPath.includes(meta.path)) return agent;
if (normalizedPath.includes(meta.path)) return agent;
}
return null;
}
export function isProxmoxSettingsPath(path: string): boolean {
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
const normalizedPath = normalizeSettingsPath(path);
return (
canonicalPath === PLATFORM_CONNECTIONS_PREFIX ||
canonicalPath.startsWith(PROXMOX_PREFIX) ||
canonicalPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
canonicalPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
normalizedPath.startsWith(PROXMOX_PREFIX) ||
normalizedPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
normalizedPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
);
}
@ -356,9 +294,8 @@ export function deriveTabFromQuery(search: string): SettingsTab | null {
case 'connections':
case 'platforms':
case 'proxmox':
return 'infrastructure-connections';
case 'install':
return 'infrastructure-install';
return 'infrastructure-systems';
case 'system-recovery':
return 'system-recovery';
case 'system-updates':
@ -420,10 +357,6 @@ export function settingsTabPath(tab: SettingsTab): string {
switch (tab) {
case 'infrastructure-systems':
return INFRASTRUCTURE_SYSTEMS_PREFIX;
case 'infrastructure-connections':
return PLATFORM_CONNECTIONS_PREFIX;
case 'infrastructure-install':
return INFRASTRUCTURE_INSTALL_PREFIX;
case 'system-recovery':
return '/settings/system-recovery';
case 'organization-overview':
@ -454,11 +387,7 @@ export function settingsTabPath(tab: SettingsTab): string {
}
export function isInfrastructureSettingsTab(tab: SettingsTab): tab is InfrastructureSettingsTab {
return (
tab === 'infrastructure-systems' ||
tab === 'infrastructure-connections' ||
tab === 'infrastructure-install'
);
return tab === 'infrastructure-systems';
}
export function buildLegacyOperationsSettingsPath(path: string): string {

View file

@ -51,14 +51,6 @@ export const createSettingsPanelRegistry = (
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
getProps: context.getInfrastructurePanelProps,
},
'infrastructure-connections': {
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
getProps: context.getInfrastructurePanelProps,
},
'infrastructure-install': {
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
getProps: context.getInfrastructurePanelProps,
},
'system-general': {
component: context.systemGeneralPanel,
},

View file

@ -4,7 +4,6 @@ import { logger } from '@/utils/logger';
import { SettingsAPI } from '@/api/settings';
import type { EventDataMap, EventType } from '@/stores/events';
import type { NodeConfigWithStatus } from '@/types/nodes';
import type { SettingsTab } from './settingsTypes';
import {
collectConfiguredInfrastructureHosts,
type DiscoveryScanStatus,
@ -37,7 +36,6 @@ type RawDiscoveredServer = {
interface UseInfrastructureDiscoveryRuntimeStateParams {
eventBus: InfrastructureEventBus;
currentTab: Accessor<SettingsTab>;
nodes: Accessor<NodeConfigWithStatus[]>;
discoveryEnabled: Accessor<boolean>;
setDiscoveryEnabled: Setter<boolean>;
@ -60,7 +58,6 @@ interface UseInfrastructureDiscoveryRuntimeStateParams {
export const useInfrastructureDiscoveryRuntimeState = ({
eventBus,
currentTab,
nodes,
discoveryEnabled,
setDiscoveryEnabled,
@ -492,11 +489,9 @@ export const useInfrastructureDiscoveryRuntimeState = ({
discoveryInterval = undefined;
}
if (currentTab() === 'infrastructure-connections') {
discoveryInterval = setInterval(() => {
void loadDiscoveredNodes();
}, 30000);
}
discoveryInterval = setInterval(() => {
void loadDiscoveredNodes();
}, 30000);
});
onCleanup(() => {

View file

@ -470,16 +470,12 @@ Pulse prepares the first-host install token from setup so you can move straight
}));
});
const openPlatformConnections = () => {
navigate(buildInfrastructureWorkspacePath('platforms'));
};
const openDashboard = () => {
navigate('/dashboard');
};
const openInfrastructureInventory = () => {
navigate(buildInfrastructureWorkspacePath('inventory'));
navigate(buildInfrastructureWorkspacePath());
};
const isEmbedded = () => options.embedded ?? false;
@ -517,7 +513,6 @@ Pulse prepares the first-host install token from setup so you can move straight
lookupValue,
lookupWasAutoDetected,
openDashboard,
openPlatformConnections,
openInfrastructureInventory,
requiresToken,
selectedAgentUrl,

View file

@ -1,7 +1,6 @@
import { Accessor, Setter, createEffect, createSignal, onCleanup, onMount } from 'solid-js';
import { logger } from '@/utils/logger';
import type { EventDataMap, EventType } from '@/stores/events';
import type { SettingsTab } from './settingsTypes';
import { useInfrastructureConfiguredNodesState } from './useInfrastructureConfiguredNodesState';
import { useInfrastructureDiscoveryRuntimeState } from './useInfrastructureDiscoveryRuntimeState';
import { useTrueNASSettingsPanelState } from './useTrueNASSettingsPanelState';
@ -19,7 +18,6 @@ type InfrastructureEventBus = {
interface UseInfrastructureSettingsStateParams {
eventBus: InfrastructureEventBus;
currentTab: Accessor<SettingsTab>;
discoveryEnabled: Accessor<boolean>;
setDiscoveryEnabled: Setter<boolean>;
discoverySubnet: Accessor<string>;
@ -46,7 +44,6 @@ interface UseInfrastructureSettingsStateParams {
export function useInfrastructureSettingsState({
eventBus,
currentTab,
discoveryEnabled,
setDiscoveryEnabled,
discoverySubnet,
@ -82,7 +79,6 @@ export function useInfrastructureSettingsState({
const discoveryRuntime = useInfrastructureDiscoveryRuntimeState({
eventBus,
currentTab,
nodes: configuredNodes.nodes,
discoveryEnabled,
setDiscoveryEnabled,

View file

@ -7,10 +7,8 @@ import {
deriveAgentFromPath,
deriveTabFromPath,
deriveTabFromQuery,
isInfrastructureSettingsTab,
isProxmoxSettingsPath,
resolveCanonicalSettingsPath,
settingsAgentPath,
settingsTabPath,
type AgentKey,
type SettingsTab,
@ -39,33 +37,16 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
const [currentTab, setCurrentTab] = createSignal<SettingsTab>(
deriveTabFromPath(location.pathname),
);
const resolveTabPath = (tab: SettingsTab): string =>
presentationPolicyIsReadOnly() &&
isInfrastructureSettingsTab(tab) &&
tab !== 'infrastructure-systems'
? buildInfrastructureWorkspacePath('inventory')
: settingsTabPath(tab);
const resolveTabPath = (tab: SettingsTab): string => settingsTabPath(tab);
const activeTab = (): SettingsTab => currentTab();
const [selectedAgent, setSelectedAgent] = createSignal<AgentKey>('pve');
const handleSelectAgent = (agent: AgentKey) => {
setSelectedAgent(agent);
if (currentTab() !== 'infrastructure-connections') {
setCurrentTab('infrastructure-connections');
}
const target = settingsAgentPath(agent);
if (target && location.pathname !== target) {
navigate(target, { scroll: false });
}
};
const setActiveTab = (tab: SettingsTab) => {
if (tab === 'infrastructure-connections' && deriveAgentFromPath(location.pathname) === null) {
setSelectedAgent('pve');
}
// Eagerly update tab state so UI reflects the click immediately,
// even before the URL change triggers the sync effect.
if (currentTab() !== tab) {
setCurrentTab(tab);
}
@ -75,7 +56,6 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
}
};
// Keep tab state in sync with canonical URLs, while preserving old deep links as aliases.
createEffect(
on(
() => [location.pathname, location.search, location.hash] as const,
@ -108,23 +88,26 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
if (
presentationPolicyIsReadOnly() &&
(path === '/settings/infrastructure' ||
path.startsWith('/settings/infrastructure/platforms') ||
path.startsWith('/settings/infrastructure/install') ||
path.startsWith('/settings/infrastructure/proxmox') ||
path.startsWith('/settings/infrastructure/api') ||
path.startsWith('/settings/infrastructure/truenas') ||
path.startsWith('/settings/infrastructure/vmware') ||
(path.startsWith('/settings/infrastructure') ||
path === '/settings/workloads' ||
path === '/settings/workloads/docker')
) {
navigate(buildInfrastructureWorkspacePath('inventory'), {
navigate(buildInfrastructureWorkspacePath(), {
replace: true,
scroll: false,
});
return;
}
// Sync Proxmox agent from the raw path before any redirect so deep
// links like /platforms/proxmox/pbs still seed the correct agent.
if (isProxmoxSettingsPath(path)) {
const agentFromPath = deriveAgentFromPath(path) ?? 'pve';
if (selectedAgent() !== agentFromPath) {
setSelectedAgent(agentFromPath);
}
}
const canonicalPath = resolveCanonicalSettingsPath(path);
if (canonicalPath && canonicalPath !== path) {
navigate(canonicalPath, {
@ -139,13 +122,6 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
if (resolved !== currentTab()) {
setCurrentTab(resolved);
}
if (isProxmoxSettingsPath(effectivePath)) {
const agentFromPath = deriveAgentFromPath(effectivePath) ?? 'pve';
if (selectedAgent() !== agentFromPath) {
setSelectedAgent(agentFromPath);
}
}
},
),
);

View file

@ -54,8 +54,7 @@ const UNIFIED_RESOURCE_GUIDANCE = {
],
} as const;
const INFRASTRUCTURE_INSTALL_PATH = '/settings/infrastructure/install';
const INFRASTRUCTURE_PLATFORMS_PATH = '/settings/infrastructure/platforms';
const INFRASTRUCTURE_PATH = '/settings/infrastructure';
const SETUP_WIZARD_TELEMETRY_SURFACE = 'setup_wizard_complete';
export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
@ -141,8 +140,7 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
const downloadCredentials = () => {
const baseUrl = getPulseBaseUrl();
const installWorkspaceUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_INSTALL_PATH}`;
const platformConnectionsUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_PLATFORMS_PATH}`;
const infrastructureUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_PATH}`;
const content = `Pulse Credentials
==================
Generated: ${new Date().toISOString()}
@ -157,23 +155,12 @@ Admin API Token:
----------------
${props.state.apiToken}
Infrastructure Install Workspace:
---------------------------------
${installWorkspaceUrl}
Infrastructure:
---------------
${infrastructureUrl}
Platform Connections Workspace:
-------------------------------
${platformConnectionsUrl}
Use the Infrastructure Install workspace to:
- continue with the first-host install token Pulse prepares from setup
- choose the agent connection URL
- configure TLS and custom CA options
- copy Linux, macOS, Windows, and related install commands
Use the Platform connections workspace when:
- the first system is API-backed, such as Proxmox, TrueNAS, or VMware
- Pulse should poll that platform directly instead of starting with a host install
Use Add infrastructure to connect your first host or API-backed platform
(Proxmox, TrueNAS, VMware, and others).
Keep these credentials secure!
`;
@ -190,11 +177,11 @@ Keep these credentials secure!
};
const handleOpenInstallWorkspace = () => {
props.onComplete(INFRASTRUCTURE_INSTALL_PATH);
props.onComplete(INFRASTRUCTURE_PATH);
};
const handleOpenPlatformConnections = () => {
props.onComplete(INFRASTRUCTURE_PLATFORMS_PATH);
props.onComplete(INFRASTRUCTURE_PATH);
};
const handleGoToDashboard = () => {
@ -414,10 +401,10 @@ Keep these credentials secure!
<div class="bg-surface-hover dark:bg-black border border-border rounded-md p-3">
<div class="text-[11px] font-medium text-muted mb-1 uppercase tracking-wider">
Infrastructure Install Workspace
Infrastructure
</div>
<code class="text-base-content font-mono text-xs break-all">
{INFRASTRUCTURE_INSTALL_PATH}
{INFRASTRUCTURE_PATH}
</code>
</div>

View file

@ -5,25 +5,16 @@ import setupCompletionModelSource from '../setupCompletionModel.ts?raw';
describe('SetupCompletionPanel guardrails', () => {
it('keeps setup completion aligned with the canonical infrastructure install workspace', () => {
expect(setupCompletionPanelSource).toContain(
"const INFRASTRUCTURE_INSTALL_PATH = '/settings/infrastructure/install';",
);
expect(setupCompletionPanelSource).toContain(
"const INFRASTRUCTURE_PLATFORMS_PATH = '/settings/infrastructure/platforms';",
"const INFRASTRUCTURE_PATH = '/settings/infrastructure';",
);
expect(setupCompletionPanelSource).toContain('Open Infrastructure Install');
expect(setupCompletionPanelSource).toContain('Open Platform connections');
expect(setupCompletionPanelSource).toContain('Infrastructure Install Workspace');
expect(setupCompletionPanelSource).toContain('Platform Connections Workspace');
expect(setupCompletionPanelSource).toContain('Credentials you must save now');
expect(setupCompletionPanelSource).toContain('Shown during setup');
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_INSTALL_PATH);');
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_PLATFORMS_PATH);');
expect(setupCompletionPanelSource).toContain('Use the Infrastructure Install workspace to:');
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_PATH);');
expect(setupCompletionPanelSource).toContain(
'continue with the first-host install token Pulse prepares from setup',
);
expect(setupCompletionPanelSource).toContain('Use the Platform connections workspace when:');
expect(setupCompletionPanelSource).toContain('configure TLS and custom CA options');
expect(setupCompletionPanelSource).not.toContain("from '@/stores/licenseCommercial';");
expect(setupCompletionPanelSource).not.toContain('runStartProTrialAction');
expect(setupCompletionPanelSource).not.toContain('loadCommercialPosture');

View file

@ -118,7 +118,7 @@ describe('SetupCompletionPanel', () => {
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/install');
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
});
it('hands API-backed starts into the canonical platform connections workspace', async () => {
@ -128,7 +128,7 @@ describe('SetupCompletionPanel', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
});
it('downloads credentials that point operators to the install workspace instead of inline commands', async () => {
@ -155,16 +155,9 @@ describe('SetupCompletionPanel', () => {
const content = await getLastDownloadedBlob().text();
expect(content).toContain('Web Login:');
expect(content).toContain('Admin API Token:');
expect(content).toContain('Infrastructure Install Workspace:');
expect(content).toContain('https://pulse.example.com/settings/infrastructure/install');
expect(content).toContain('Platform Connections Workspace:');
expect(content).toContain('https://pulse.example.com/settings/infrastructure/platforms');
expect(content).toContain(
'continue with the first-host install token Pulse prepares from setup',
);
expect(content).toContain(
'the first system is API-backed, such as Proxmox, TrueNAS, or VMware',
);
expect(content).toContain('Infrastructure:');
expect(content).toContain('https://pulse.example.com/settings/infrastructure');
expect(content).toContain('Use Add infrastructure to connect');
expect(content).not.toContain('Example Install Command');
expect(content).not.toContain('Example Windows Install Command');
@ -291,7 +284,7 @@ describe('SetupCompletionPanel', () => {
name: 'Open Platform connections',
}),
);
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
});
it('derives admitted VMware onboarding from the governed platform manifest', async () => {
@ -331,7 +324,7 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
});
it('keeps both continuation paths visible when install-managed and API-backed systems are already present', async () => {

View file

@ -58,7 +58,7 @@ describe('SetupCompletionPreview', () => {
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/install');
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure');
});
it('renders the VMware-connected preview scenario without polling runtime state', () => {

View file

@ -185,7 +185,7 @@ describe('Dashboard page module contract', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open infrastructure install' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
});
it('renders the governed storage and recovery dashboard panels', () => {

View file

@ -61,6 +61,6 @@ describe('Infrastructure empty state', () => {
fireEvent.click(getByText('Open Infrastructure Install'));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
});
});

View file

@ -10,7 +10,7 @@ describe('buildStorageRecoveryTabSpecs', () => {
expect(specs.map((spec) => spec.label)).toEqual(['Storage', 'Recovery']);
expect(specs.map((spec) => spec.route)).toEqual([buildStoragePath(), buildRecoveryPath()]);
expect(specs.map((spec) => spec.settingsRoute)).toEqual([
'/settings/infrastructure/platforms/proxmox/pbs',
'/settings/infrastructure',
'/settings/system-recovery',
]);
expect(specs.every((spec) => spec.badge === undefined)).toBe(true);

View file

@ -19,7 +19,7 @@ export function buildStorageRecoveryTabSpecs(): StorageRecoveryTabSpec[] {
id: 'storage',
label: 'Storage',
route: buildStoragePath(),
settingsRoute: '/settings/infrastructure/platforms/proxmox/pbs',
settingsRoute: '/settings/infrastructure',
tooltip: 'Storage pools, disks, and datastores',
},
{

View file

@ -3425,7 +3425,7 @@ describe('frontend resource type boundaries', () => {
);
expect(infrastructureWorkspaceSource).not.toContain('inferViewFromPath');
expect(infrastructureWorkspaceModelSource).toContain(
'export function getInfrastructureWorkspaceViewFromPath',
'export function deriveAddStepFromLegacyPath',
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function buildInfrastructureWorkspacePath',