Retire legacy settings infrastructure aliases

This commit is contained in:
rcourtman
2026-05-25 14:07:17 +01:00
parent 3c0bd12fc6
commit cfb16e6da1
22 changed files with 225 additions and 387 deletions
@@ -87,14 +87,13 @@ management, and fleet control surfaces.
60. `frontend-modern/src/utils/clusterEndpointPresentation.ts`
61. `frontend-modern/src/utils/nodeModalPresentation.ts`
62. `frontend-modern/src/utils/proxmoxSettingsPresentation.ts`
63. `frontend-modern/src/components/Settings/platformConnectionsModel.ts`
64. `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`
65. `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`
66. `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx`
67. `internal/hostagent/proxmox_setup.go`
68. `internal/remoteconfig/client.go`
69. `internal/agenttls/config.go`
70. `internal/api/agent_exec_token_binding.go`
63. `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`
64. `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`
65. `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx`
66. `internal/hostagent/proxmox_setup.go`
67. `internal/remoteconfig/client.go`
68. `internal/agenttls/config.go`
69. `internal/api/agent_exec_token_binding.go`
## Shared Boundaries
@@ -629,7 +628,7 @@ profile and assignment columns, but embedded table framing must route through
Approval-gated command execution must expose stable rejection reasons for
invalid approval grants so fleet operators can distinguish missing, expired,
mismatched, and signature-invalid grants through agent metrics.
9. Add or change profile management, the extracted agent profiles runtime owner, the agent profile settings catalog, the infrastructure source-manager landing, the pure unified-agent inventory/install model, the connections-ledger workspace shell, the unified ConnectionEditor and its per-type credential slots, route model, shared install section owner, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system impact preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/agentProfileSettings.ts`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/connectionsTableModel.ts`, `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, `frontend-modern/src/components/Settings/useConnectionRowActions.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. Phase 9 retired the legacy reporting/inventory surface (InfrastructureOperationsController, InfrastructureInventorySection, InfrastructureActiveRowDetails, InfrastructureIgnoredRowDetails, InfrastructureStopMonitoringDialog, useInfrastructureReportingState) and the per-type shells (PlatformConnectionsWorkspace, ProxmoxSettingsPanel, ProxmoxDirectWorkspace, ProxmoxConfiguredNodesTable, ProxmoxDirectConnectionsCard, ProxmoxDiscoveryResultsCard, ProxmoxDeleteNodeDialog, ProxmoxNodeModalStack, NodeModal shell, TrueNASSettingsPanel, VMwareSettingsPanel, useProxmoxDirectWorkspaceState); lifecycle extensions must route through the unified aggregator ledger, source-manager cards, and ConnectionEditor credential slots rather than reintroducing those retired surfaces.
9. Add or change profile management, the extracted agent profiles runtime owner, the agent profile settings catalog, the infrastructure source-manager landing, the pure unified-agent inventory/install model, the connections-ledger workspace shell, the unified ConnectionEditor and its per-type credential slots, route model, shared install section owner, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system impact preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/agentProfileSettings.ts`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/connectionsTableModel.ts`, `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, `frontend-modern/src/components/Settings/useConnectionRowActions.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. Phase 9 retired the legacy reporting/inventory surface (InfrastructureOperationsController, InfrastructureInventorySection, InfrastructureActiveRowDetails, InfrastructureIgnoredRowDetails, InfrastructureStopMonitoringDialog, useInfrastructureReportingState) and the per-type shells (PlatformConnectionsWorkspace, ProxmoxSettingsPanel, ProxmoxDirectWorkspace, ProxmoxConfiguredNodesTable, ProxmoxDirectConnectionsCard, ProxmoxDiscoveryResultsCard, ProxmoxDeleteNodeDialog, ProxmoxNodeModalStack, NodeModal shell, TrueNASSettingsPanel, VMwareSettingsPanel, useProxmoxDirectWorkspaceState); lifecycle extensions must route through the unified aggregator ledger, source-manager cards, and ConnectionEditor credential slots rather than reintroducing those retired surfaces.
Those lifecycle-owned settings hooks may consume websocket state only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx` or recreate root-shell providers.
Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the infrastructure source-management toolbar too: the landing page exposes `Add infrastructure`, `Run discovery`, and `Discovery settings` as first-viewport toolbar actions inside the source manager, while governed source rows expose per-source add actions such as `Install Pulse Agent` from the shared catalog. `Run discovery` must remain a visible text command for manual scans rather than an icon-only affordance. `Detect address` remains inside the single add-flow source picker/probe path rather than a duplicate toolbar action. The same landing boundary may surface setup confidence from the unified rows and discovered candidates, including connected-system count, API coverage, agent coverage, sources that still need an agent, and discovery review state, without creating a second inventory model or provider-specific summary fetch. Source groups must stay in the governed source catalog order instead of re-sorting by current row count, row-level lifecycle entry points must use `Manage` language, and locked agent-install states must show a compact command inventory without raw token placeholders or disabled copy commands until a token exists. Network discovery settings remain safety-critical: automatic scanning must surface the shared-network/subnet warning before operators save scan mode changes. The AI settings manual workload refresh at `/api/discovery/run` is a discovery/API refresh sweep, not an enrollment or source-admission path; lifecycle surfaces may observe the refreshed candidates but must not reinterpret it as agent install authority. Agent command-execution handoff copy belongs to that same install surface: `InfrastructureInstallerSection.tsx` may expose the Pulse command-execution toggle for Patrol, but the label must describe Patrol remediation rather than reviving `Patrol auto-fix` or implying a paid monitoring-volume gate.
Setup-completion handoff belongs to that same single add-flow boundary. The first-run completion screen must keep credentials as the first surfaced object, then present one compact next-step surface that sends operators to Add infrastructure or directly to the Agent handoff; source-choice explanation may live inside that surface, but lifecycle work must not reintroduce a separate setup-wizard tour, duplicate CTA section, or inline install-command owner before the canonical infrastructure workspace.
@@ -964,13 +963,14 @@ profile and assignment columns, but embedded table framing must route through
and opens the saved-connection create flow for API-backed platforms on the
same page. `/settings/infrastructure/install`,
`/settings/infrastructure/platforms`, and
`/settings/infrastructure/operations` remain valid deep links, but they
must resolve to section focus on that same single-page workspace rather
than rendering separate page shells or hiding the top ledger. Read-only
sessions must redirect any non-inventory infrastructure deep link back to
`/settings/infrastructure`, suppress the add-system entry point, and hide
configuration-only sections so presentation-policy restrictions still
hold. That top ledger must also stay readable inside the governed settings
`/settings/infrastructure/operations` are retired settings aliases and must
fail route eligibility instead of resolving to section focus or rendering
separate page shells. The canonical add flow is
`/settings/infrastructure?add=<step>`. Read-only sessions must redirect the
canonical Infrastructure workspace back to `/settings/infrastructure`,
suppress the add-system entry point, and hide configuration-only sections so
presentation-policy restrictions still hold. That top ledger must also stay
readable inside the governed settings
shell at ordinary desktop widths: `InfrastructureSourceManager.tsx` must
keep the compact row/card presentation readable without forcing horizontal
scrolling just to reach primary controls. Dedicated collection and
@@ -1243,8 +1243,8 @@ confirmation, preflight, retry, and status labels.
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
add flow state is carried only by the `add` query parameter on that base path.
`SetupCompletionPanel.tsx` uses the shared infrastructure path builders for all
install and platform CTAs.
`frontend-modern/src/utils/infrastructureSettingsPresentation.ts` owns the
customer-facing Settings Infrastructure target label and onboarding source
@@ -1768,8 +1768,8 @@ The API-backed platform onboarding surface now lives inside the shared
Infrastructure workspace. `InfrastructureSourceManager.tsx`,
`connectionsTableModel.ts`, `InfrastructureWorkspace.tsx`,
`infrastructureWorkspaceModel.ts`, `InfrastructureInstallerSection.tsx`,
`platformConnectionsModel.ts`, `useInfrastructureSettingsState.ts`,
`useTrueNASSettingsPanelState.ts`, `useVMwareSettingsPanelState.ts`,
`useInfrastructureSettingsState.ts`, `useTrueNASSettingsPanelState.ts`,
`useVMwareSettingsPanelState.ts`,
`proxmoxSettingsModel.ts`, `useInfrastructureConfiguredNodesState.ts`, and
`useInfrastructureDiscoveryRuntimeState.ts` own the fallback
install/direct/reporting operator flow, with `InfrastructureSourceManager.tsx`
@@ -3249,17 +3249,15 @@ The infrastructure workspace now uses a single flat route
modal is open and which add step is active — is managed through the governed
`InfrastructurePanelStep` query contract 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.
exposes `buildInfrastructureWorkspacePath()` and
`buildInfrastructureOnboardingPath()` as the sole path-building contract for
lifecycle-adjacent install and setup surfaces. Callers that formerly passed
`'platforms'` or `'install'` to `buildInfrastructureWorkspacePath` must use the
no-argument form or the query-backed onboarding builder; removed subpaths must
not remain as compatibility hrefs.
`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
shared infrastructure path-builder constants for workspace, add-infrastructure,
and agent-install CTAs, replacing the former pair of
`INFRASTRUCTURE_INSTALL_PATH` and `INFRASTRUCTURE_PLATFORMS_PATH` constants.
Installer-owned runtime continuity on persistence-sensitive NAS platforms is
also explicit again: `scripts/install.sh` now owns the QNAP bootstrap contract
@@ -676,7 +676,10 @@ platform/runtime surfaces must stay hidden from the AI-adjacent shell and must
not be kept as disabled placeholders. The legacy `/infrastructure` route shell
was retired alongside its page wrapper, and `/workloads`, `/storage`,
`/recovery`, `/ceph`, `/ai`, and `/operations/*` remain unregistered
top-level routes. The AI Chat launcher, Patrol surfaces, and
top-level routes. Primary platform tab `settingsRoute` handoffs must also point
to the canonical `/settings/infrastructure` workspace rather than retired
settings aliases such as `/settings/workloads/docker` or nested
`/settings/infrastructure/platforms/*` paths. The AI Chat launcher, Patrol surfaces, and
`AssistantHandoffPayload` deep links must use canonical platform or runtime
routes (`/proxmox/overview`, `/proxmox/storage`, `/kubernetes/workloads`, etc.)
rather than reviving retired Infrastructure or aggregate workspace paths;
@@ -220,7 +220,7 @@ output or bypass the normal non-admin redaction path.
21. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` shared with `security-privacy`: the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary.
22. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts` shared with `agent-lifecycle`: the direct-node infrastructure settings state hook is both an agent lifecycle control surface and a shared Proxmox node API contract boundary.
23. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts` shared with `agent-lifecycle`: the infrastructure discovery runtime state hook is both an agent lifecycle control surface and a shared discovery/settings API contract boundary.
That same shared boundary also owns settings-route polling scope for discovery payloads: the `/api/discover` refresh loop and websocket-backed discovery status hydration may run only while the operator is on the infrastructure connections workspace under `/settings/infrastructure/platforms*`, not on the systems ledger or install workspace.
That same shared boundary also owns settings-route polling scope for discovery payloads: the `/api/discover` refresh loop and websocket-backed discovery status hydration may run only while the operator is on the canonical Infrastructure settings workspace at `/settings/infrastructure`, not on retired infrastructure sub-routes.
Discovery refreshes and scan results are route-scoped state updates. If the
settings route unmounts while a request is in flight, the hook must drop the
result and avoid surfacing stale background errors into the next settings
@@ -512,7 +512,11 @@ or other self-hosted uncapped continuity plans.
paid or hosted app shell. Infrastructure is not an equal primary tab in
that list. Workloads, Storage, and Recovery are not standalone aggregate
workspace tabs; their component surfaces remain reusable inside platform
pages via embedded `tableOnly` surfaces. Each platform page must remain
pages via embedded `tableOnly` surfaces. Platform primary-tab settings
handoffs in `AppLayout.tsx` must target the canonical
`/settings/infrastructure` workspace and must not retain retired settings
aliases such as `/settings/workloads/docker` or nested
`/settings/infrastructure/platforms/*` paths. Each platform page must remain
chrome-only:
routing plus sub-tab navigation that embeds the canonical
`WorkloadsSurface`, `StorageSurface`, `RecoverySurface`, or
@@ -1401,10 +1401,11 @@ not a replacement status card, CTA band, or page-local nested card.
`frontend-modern/src/App.tsx` must land authenticated `/` and `/login`
handoffs on the first visible provider/runtime platform in the canonical
shell order: Proxmox, Containers, Kubernetes, TrueNAS, vSphere, then Agents
only for agent-only estates. Legacy Infrastructure remains route-compatible
and may still guide first-time operators into `Add infrastructure`, but it
is not the default operational landing surface once provider evidence is
present.
only for agent-only estates. The retired Infrastructure aggregate route and
nested settings infrastructure aliases are not compatibility commitments:
first-time operator setup must enter through the canonical Settings →
Infrastructure workspace and its query-backed add flow, while provider
evidence still owns the operational landing surface.
The authenticated app shell's boot-time route preloads must be owned by
`frontend-modern/src/routing/routePreload.ts` so top-level cold-tab
readiness cannot drift from the route-module preloader. Workloads,
@@ -2796,8 +2797,8 @@ shared discovery draft and subnet-validation state,
owns infrastructure workspace prop assembly and resource-derived infrastructure
read-model shaping for the shell,
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` owns
settings tab identity, canonical route derivation, legacy alias normalization,
and Proxmox agent route metadata. `settingsRouting.ts` and
settings tab identity, canonical route derivation, route eligibility, and
retired infrastructure/workloads alias rejection. `settingsRouting.ts` and
`settingsTypes.ts` remain thin compatibility re-export shims only, so external
consumers can bridge to the canonical owner without reintroducing a second
settings navigation model. `settingsNavCatalog.ts` owns settings navigation
@@ -2809,13 +2810,14 @@ including the route-owned billing focus contract where
`/settings/system/billing/plan` is the canonical settings-tab destination,
`/settings/system/billing/usage` is a same-tab child state, and legacy billing
base/hash links are compatibility inputs rather than primary runtime routes.
That same route-sync owner must also preserve Proxmox platform-selection truth
across canonical deep links such as
`/settings/infrastructure/platforms/proxmox/pbs` and
`/settings/infrastructure/platforms/proxmox/pmg`: even though those routes
collapse into the shared `infrastructure-operations` tab, the selected
platform state must still be derived from the path instead of silently
falling back to `pve` on reload or remount.
Infrastructure settings no longer has route-level platform-selection state:
`/settings/infrastructure` and `/settings/infrastructure?add=<step>` are the
only routeable Infrastructure settings entry points. Former nested aliases such
as `/settings/infrastructure/install`,
`/settings/infrastructure/platforms/proxmox/pbs`,
`/settings/infrastructure/api/pve`, and `/settings/workloads/docker` must fail
route eligibility instead of being normalized back into the Infrastructure
workspace.
That same settings access boundary must keep route eligibility separate from
sidebar visibility. Panel-owned feature gates such as Relay, Reporting, RBAC,
Audit Log, and Audit Webhooks may be hidden from the navigation on Community
@@ -3533,17 +3535,16 @@ 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
and `settingsNavigationModel.ts`. 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.
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` owns the
explicit routeability check that rejects retired infrastructure/workloads
aliases before the settings shell mounts. `useSettingsNavigation.ts` may
redirect `/settings` and still canonicalize current settings destinations, but
it must not translate removed infrastructure subpaths into onboarding queries or
derive Proxmox platform state from those paths.
The shared frontend source/platform vocabulary now also includes
`availability` as an agentless infrastructure source and `network-endpoint` as
the canonical resource projection. Picker cards, source labels, badges, and
@@ -718,7 +718,6 @@
"frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx",
"frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx",
"frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx",
"frontend-modern/src/components/Settings/platformConnectionsModel.ts",
"frontend-modern/src/components/Settings/proxmoxSettingsModel.ts",
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
"frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts",
@@ -1054,7 +1053,6 @@
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
"frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx",
"frontend-modern/src/components/Settings/platformConnectionsModel.ts",
"frontend-modern/src/components/Settings/proxmoxSettingsModel.ts",
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
"frontend-modern/src/components/Settings/useConnectionRowActions.ts",
+8 -6
View File
@@ -50,6 +50,7 @@ import { isPro } from '@/stores/licenseCommercial';
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
import { AI_CHAT_LAUNCHER_ARIA_LABEL, getAIChatLauncherTitle } from '@/utils/aiChatPresentation';
import type { AppConnectionStatus } from '@/useAppRuntimeState';
import { buildInfrastructureWorkspacePath } from '@/components/Settings/infrastructureWorkspaceModel';
const ROOT_PROXMOX_PATH = buildProxmoxPath();
const ROOT_DOCKER_PATH = buildDockerPath();
@@ -57,6 +58,7 @@ const ROOT_KUBERNETES_PATH = buildKubernetesPath();
const ROOT_TRUENAS_PATH = buildTrueNASPath();
const ROOT_VMWARE_PATH = buildVmwarePath();
const ROOT_AGENTS_PATH = buildAgentsPath();
const ROOT_INFRASTRUCTURE_SETTINGS_PATH = buildInfrastructureWorkspacePath();
const ROOT_ALERTS_PATH = '/alerts';
const NAV_TAB_ICON_CLASS = 'w-4 h-4 shrink-0';
const AI_CHAT_LAUNCHER_BUTTON_CLASS =
@@ -332,7 +334,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'proxmox',
label: 'Proxmox',
route: ROOT_PROXMOX_PATH,
settingsRoute: '/settings/infrastructure/platforms/proxmox/pve',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'Proxmox VE, Backup Server, Mail Gateway, storage, backups, and guests',
enabled: isVisible('proxmox'),
live: isVisible('proxmox'),
@@ -343,7 +345,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'docker',
label: 'Containers',
route: ROOT_DOCKER_PATH,
settingsRoute: '/settings/workloads/docker',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'Docker / Podman runtime lens: hosts, containers, and Swarm services',
enabled: isVisible('docker'),
live: isVisible('docker'),
@@ -354,7 +356,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'kubernetes',
label: 'Kubernetes',
route: ROOT_KUBERNETES_PATH,
settingsRoute: '/settings',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'Kubernetes clusters, nodes, pods, deployments, and services',
enabled: isVisible('kubernetes'),
live: isVisible('kubernetes'),
@@ -365,7 +367,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'truenas',
label: 'TrueNAS',
route: ROOT_TRUENAS_PATH,
settingsRoute: '/settings/infrastructure',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'TrueNAS hosts, storage, and apps',
enabled: isVisible('truenas'),
live: isVisible('truenas'),
@@ -376,7 +378,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'vmware',
label: 'vSphere',
route: ROOT_VMWARE_PATH,
settingsRoute: '/settings/infrastructure',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'VMware vSphere hosts, virtual machines, datastores, and networks',
enabled: isVisible('vmware'),
live: isVisible('vmware'),
@@ -387,7 +389,7 @@ export function AppLayout(props: AppLayoutProps) {
id: 'agents',
label: 'Agents',
route: ROOT_AGENTS_PATH,
settingsRoute: '/settings/infrastructure',
settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH,
tooltip: 'Standalone Pulse Agent machines, OS telemetry, storage, and command eligibility',
enabled: isVisible('agents'),
live: isVisible('agents'),
@@ -46,7 +46,9 @@ describe('App architecture', () => {
expect(appSource).not.toContain('const WorkloadsPage = lazy(');
expect(appSource).not.toContain('const StoragePage = lazy(');
expect(appSource).not.toContain('const RecoveryPage = lazy(');
expect(appSource).not.toContain("import('./features/infrastructure/InfrastructurePageSurface')");
expect(appSource).not.toContain(
"import('./features/infrastructure/InfrastructurePageSurface')",
);
expect(appSource).not.toContain('component={InfrastructurePage}');
expect(appSource).not.toContain('INFRASTRUCTURE_PATH');
expect(appSource).not.toContain('<Route path={WORKLOADS_PATH}');
@@ -110,7 +112,7 @@ describe('App architecture', () => {
expect(appLayoutSource).not.toContain("id: 'workloads',");
expect(appLayoutSource).not.toContain("id: 'storage',");
expect(appLayoutSource).not.toContain("id: 'recovery',");
expect(appLayoutSource).not.toContain("aria-label=\"Workspaces\"");
expect(appLayoutSource).not.toContain('aria-label="Workspaces"');
expect(appLayoutSource).not.toContain('buildStorageRecoveryTabSpecs(');
expect(appSource).not.toContain('DashboardPage');
expect(headerAuditSource).not.toContain("['src/pages/Dashboard.tsx', 'PageHeader']");
@@ -183,6 +185,15 @@ describe('App architecture', () => {
expect(appLayoutSource).toContain(
"import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState';",
);
expect(appLayoutSource).toContain(
"import { buildInfrastructureWorkspacePath } from '@/components/Settings/infrastructureWorkspaceModel';",
);
expect(appLayoutSource).toContain(
'const ROOT_INFRASTRUCTURE_SETTINGS_PATH = buildInfrastructureWorkspacePath();',
);
expect(appLayoutSource).toContain('settingsRoute: ROOT_INFRASTRUCTURE_SETTINGS_PATH');
expect(appLayoutSource).not.toContain("settingsRoute: '/settings/workloads");
expect(appLayoutSource).not.toContain("settingsRoute: '/settings/infrastructure/platforms");
expect(appLayoutSource).toContain('<OrgSwitcher');
expect(appLayoutSource).toContain('const status = () => props.connectionStatus();');
expect(appLayoutSource).toContain(
@@ -17,6 +17,8 @@ import { useSettingsSystemPanels } from './useSettingsSystemPanels';
import { useSystemSettingsState } from './useSystemSettingsState';
import { useSettingsNavigation } from './useSettingsNavigation';
import { getSettingsLoadingState } from '@/utils/settingsShellPresentation';
import { isRouteableSettingsPath } from './settingsNavigationModel';
import NotFound from '@/pages/NotFound';
import { getRuntimeLimit, loadRuntimeCapabilities } from '@/stores/license';
import { isPro } from '@/stores/licenseCommercial';
@@ -33,7 +35,7 @@ const SettingsPanelLoadingFallback = () => (
</div>
);
const Settings: Component<SettingsProps> = (props) => {
const SettingsWorkspace: Component<SettingsProps> = (props) => {
const { state, connected: _connected } = useWebSocket();
const navigate = useNavigate();
const location = useLocation();
@@ -219,4 +221,15 @@ const Settings: Component<SettingsProps> = (props) => {
);
};
const Settings: Component<SettingsProps> = (props) => {
const location = useLocation();
const isRouteableSettingsRoute = createMemo(() => isRouteableSettingsPath(location.pathname));
return (
<Show when={isRouteableSettingsRoute()} fallback={<NotFound />}>
<SettingsWorkspace {...props} />
</Show>
);
};
export default Settings;
@@ -166,6 +166,10 @@ describe('infrastructure operations model', () => {
expect(hasMachineInstallActions(row)).toBe(false);
expect(getCapabilitySurfaceLabel('availability')).toBe('Availability data');
expect(getCapabilityManagementPath('availability')).toBe('/settings/infrastructure');
expect(getCapabilityManagementPath('proxmox')).toBe('/settings/infrastructure');
expect(getCapabilityManagementPath('pbs')).toBe('/settings/infrastructure');
expect(getCapabilityManagementPath('pmg')).toBe('/settings/infrastructure');
expect(getCapabilityManagementPath('truenas')).toBe('/settings/infrastructure');
expect(getPlatformConnectionsViewForCapability('availability')).toBeNull();
});
@@ -3,7 +3,6 @@ import {
buildInfrastructureOnboardingPath,
buildInfrastructureWorkspacePath,
deriveAddStepFromLocation,
deriveAddStepFromLegacyPath,
} from '../infrastructureWorkspaceModel';
describe('infrastructureWorkspaceModel', () => {
@@ -20,29 +19,20 @@ describe('infrastructureWorkspaceModel', () => {
expect(buildInfrastructureOnboardingPath('truenas')).toBe(
'/settings/infrastructure?add=truenas',
);
expect(buildInfrastructureOnboardingPath('unraid')).toBe(
'/settings/infrastructure?add=unraid',
);
expect(buildInfrastructureOnboardingPath('docker')).toBe(
'/settings/infrastructure?add=docker',
);
expect(buildInfrastructureOnboardingPath('vmware')).toBe(
'/settings/infrastructure?add=vmware',
);
expect(buildInfrastructureOnboardingPath('unraid')).toBe('/settings/infrastructure?add=unraid');
expect(buildInfrastructureOnboardingPath('docker')).toBe('/settings/infrastructure?add=docker');
expect(buildInfrastructureOnboardingPath('vmware')).toBe('/settings/infrastructure?add=vmware');
});
it('derives add steps from onboarding deep links', () => {
expect(deriveAddStepFromLegacyPath('/settings/infrastructure/install')).toBe('linux-host');
expect(deriveAddStepFromLegacyPath('/settings/infrastructure/platforms')).toBe('pick');
expect(deriveAddStepFromLegacyPath('/settings/infrastructure/platforms/truenas')).toBeNull();
it('derives add steps only from the canonical infrastructure workspace query', () => {
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=agent')).toBe('agent');
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=pick')).toBe('pick');
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=truenas')).toBe(
'truenas',
);
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=truenas')).toBe('truenas');
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=unraid')).toBe('unraid');
expect(deriveAddStepFromLocation('/settings/infrastructure', '?add=kubernetes')).toBe(
'kubernetes',
);
expect(deriveAddStepFromLocation('/settings/infrastructure/install', '')).toBeNull();
expect(deriveAddStepFromLocation('/settings/infrastructure/platforms', '?add=pick')).toBeNull();
});
});
@@ -95,7 +95,7 @@ describe('settings architecture guardrails', () => {
expect(settingsDialogsSource).toContain('export const SettingsDialogs');
});
it('keeps infrastructure onboarding route-backed under the shared settings shell', () => {
it('keeps infrastructure onboarding query-backed under the shared settings shell', () => {
expect(settingsHeaderMetaSource).toContain("'infrastructure-systems': {");
expect(settingsHeaderMetaSource).toContain(
'Add, discover, and verify the infrastructure Pulse monitors.',
@@ -105,10 +105,10 @@ describe('settings architecture guardrails', () => {
'Manage organization invitations, member roles, and ownership transfers.',
);
expect(settingsNavigationHookSource).toContain('deriveAddStepFromLegacyPath(path)');
expect(settingsNavigationHookSource).toContain(
'navigate(buildInfrastructureOnboardingPath(infrastructureOnboardingStep), {',
);
expect(settingsSource).toContain("import NotFound from '@/pages/NotFound';");
expect(settingsSource).toContain('isRouteableSettingsPath(location.pathname)');
expect(settingsNavigationHookSource).not.toContain('deriveAddStepFromLegacyPath');
expect(settingsNavigationHookSource).not.toContain('buildInfrastructureOnboardingPath');
expect(settingsNavigationHookSource).toContain(
'navigate(buildInfrastructureWorkspacePath(), {',
);
@@ -120,9 +120,7 @@ describe('settings architecture guardrails', () => {
expect(infrastructureWorkspaceModelSource).toContain(
'export function buildInfrastructureOnboardingPath(',
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function deriveAddStepFromLegacyPath(',
);
expect(infrastructureWorkspaceModelSource).not.toContain('deriveAddStepFromLegacyPath');
expect(infrastructureWorkspaceModelSource).toContain(
'export function deriveAddStepFromSearch(',
);
@@ -3,13 +3,13 @@ import {
buildLegacyOperationsSettingsPath,
agentKeyFromPlatformType,
DEFAULT_SETTINGS_TAB,
deriveAgentFromPath,
deriveTabFromPath,
deriveTabFromQuery,
isRetiredSettingsCompatibilityPath,
isRouteableSettingsPath,
resolveCanonicalSettingsPath,
settingsAgentLabel,
settingsAgentNodeLabel,
settingsAgentPath,
settingsAgentPlatformType,
settingsTabPath,
type SettingsTab,
@@ -66,12 +66,8 @@ describe('settingsNavigationModel', () => {
it('resolves only canonical settings paths', () => {
expect(resolveCanonicalSettingsPath('/settings')).toBe('/settings/infrastructure');
expect(resolveCanonicalSettingsPath('/settings/workloads')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/workloads/docker')).toBe(
'/settings/infrastructure',
);
expect(resolveCanonicalSettingsPath('/settings/workloads')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/workloads/docker')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/support')).toBe('/settings/support/diagnostics');
expect(resolveCanonicalSettingsPath('/settings/system-updates')).toBe(
'/settings/system-updates',
@@ -79,29 +75,13 @@ 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',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms')).toBe(
'/settings/infrastructure',
);
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/infrastructure/install')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms/truenas')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/operations')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api/pve')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/infrastructure/proxmox')).toBeNull();
expect(resolveCanonicalSettingsPath('/settings/integrations/api')).toBe(
'/settings/security/api',
);
@@ -123,6 +103,31 @@ describe('settingsNavigationModel', () => {
expect(resolveCanonicalSettingsPath('/not-settings')).toBeNull();
});
it('keeps retired infrastructure settings aliases out of routeable settings paths', () => {
const retiredPaths = [
'/settings/workloads',
'/settings/workloads/docker',
'/settings/workloads/docker/agents',
'/settings/infrastructure/install',
'/settings/infrastructure/platforms',
'/settings/infrastructure/platforms/proxmox/pve',
'/settings/infrastructure/api/pve',
'/settings/infrastructure/proxmox',
'/settings/infrastructure/truenas',
'/settings/infrastructure/vmware',
];
for (const path of retiredPaths) {
expect(isRetiredSettingsCompatibilityPath(path)).toBe(true);
expect(isRouteableSettingsPath(path)).toBe(false);
}
expect(isRouteableSettingsPath('/settings')).toBe(true);
expect(isRouteableSettingsPath('/settings/infrastructure')).toBe(true);
expect(isRouteableSettingsPath('/settings/security/api')).toBe(true);
expect(isRouteableSettingsPath('/settings/unknown')).toBe(false);
});
it('maps organization routing contracts', () => {
const organizationCases: Array<[string, SettingsTab]> = [
['/settings/organization', 'organization-overview'],
@@ -143,13 +148,6 @@ describe('settingsNavigationModel', () => {
it('maps query deep-links contract values', () => {
const queryCases: Array<[string, SettingsTab | null]> = [
['?tab=infrastructure', 'infrastructure-systems'],
['?tab=agents', 'infrastructure-systems'],
['?tab=workloads', 'infrastructure-systems'],
['?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'],
['?tab=system-pro', 'system-billing'],
@@ -163,6 +161,9 @@ describe('settingsNavigationModel', () => {
['?tab=security-overview', 'security-overview'],
['?tab=data-handling', 'security-data-handling'],
['?tab=resource-privacy', 'security-data-handling'],
['?tab=workloads', null],
['?tab=install', null],
['?tab=docker', null],
['?tab=unknown', null],
];
for (const [query, expectedTab] of queryCases) {
@@ -198,36 +199,15 @@ describe('settingsNavigationModel', () => {
expect(isTabLocked('infrastructure-systems', hasFeatures([]), () => true)).toBe(false);
});
it('maps deriveAgentFromPath contracts for canonical infrastructure routes', () => {
const agentCases: Array<[string, 'pve' | 'pbs' | 'pmg' | null]> = [
// 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'],
['/settings/infrastructure', null],
['/settings/infrastructure/operations', null],
];
for (const [path, expectedAgent] of agentCases) {
expect(deriveAgentFromPath(path)).toBe(expectedAgent);
}
});
it('maps settings agent keys to canonical platform types, labels, and paths', () => {
expect(settingsAgentPath('pve')).toBe('/settings/infrastructure/platforms/proxmox/pve');
it('maps settings agent keys to platform types and labels', () => {
expect(settingsAgentPlatformType('pve')).toBe('proxmox-pve');
expect(settingsAgentLabel('pve')).toBe('Proxmox VE');
expect(settingsAgentNodeLabel('pve')).toBe('Proxmox VE node');
expect(settingsAgentPath('pbs')).toBe('/settings/infrastructure/platforms/proxmox/pbs');
expect(settingsAgentPlatformType('pbs')).toBe('proxmox-pbs');
expect(settingsAgentLabel('pbs')).toBe('Proxmox Backup Server');
expect(settingsAgentNodeLabel('pbs')).toBe('Proxmox Backup Server');
expect(settingsAgentPath('pmg')).toBe('/settings/infrastructure/platforms/proxmox/pmg');
expect(settingsAgentPlatformType('pmg')).toBe('proxmox-pmg');
expect(settingsAgentLabel('pmg')).toBe('Proxmox Mail Gateway');
expect(settingsAgentNodeLabel('pmg')).toBe('Proxmox Mail Gateway');
@@ -243,18 +223,6 @@ describe('settingsNavigationModel', () => {
expect(agentKeyFromPlatformType('agent')).toBeNull();
});
it('treats proxmox deep links as infrastructure aliases', () => {
expect(deriveTabFromPath('/settings/infrastructure/platforms')).toBe('infrastructure-systems');
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox')).toBe(
'infrastructure-systems',
);
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox/pve')).toBe(
'infrastructure-systems',
);
expect(deriveTabFromPath('/settings/infrastructure/api/pve')).toBe('infrastructure-systems');
expect(deriveTabFromPath('/settings/infrastructure/install')).toBe('infrastructure-systems');
});
it('maps support and legacy settings operations routes back to support tabs', () => {
expect(deriveTabFromPath('/settings/support/diagnostics')).toBe('support-diagnostics');
expect(deriveTabFromPath('/settings/support/reporting')).toBe('support-reporting');
@@ -80,52 +80,28 @@ describe('useSettingsNavigation', () => {
});
});
it('syncs the selected proxmox agent from canonical deep links on initial load', async () => {
it('does not derive selected agents from retired infrastructure deep links', async () => {
renderHarness('/settings/infrastructure/platforms/proxmox/pbs');
await waitFor(() => {
expect(screen.getByTestId('selected-agent')).toHaveTextContent('pbs');
});
});
it('defaults the selected proxmox agent to pve on the base proxmox route', async () => {
renderHarness('/settings/infrastructure/platforms/proxmox');
await waitFor(() => {
expect(screen.getByTestId('selected-agent')).toHaveTextContent('pve');
});
expect(navigateSpy).not.toHaveBeenCalled();
});
it('translates the legacy install route into the canonical onboarding query', async () => {
it('does not translate retired infrastructure setup paths', async () => {
renderHarness('/settings/infrastructure/install');
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=linux-host', {
replace: true,
scroll: false,
});
expect(navigateSpy).not.toHaveBeenCalled();
});
});
it('translates the legacy platform chooser route into the canonical onboarding query', async () => {
renderHarness('/settings/infrastructure/platforms');
it('does not translate retired workloads settings paths', async () => {
renderHarness('/settings/workloads/docker');
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=pick', {
replace: true,
scroll: false,
});
});
});
it('keeps legacy platform-management paths out of onboarding mode', async () => {
renderHarness('/settings/infrastructure/platforms/truenas');
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
replace: true,
scroll: false,
});
expect(navigateSpy).not.toHaveBeenCalled();
});
});
});
@@ -2,11 +2,11 @@ import type { JSX } from 'solid-js';
import type { ConnectedInfrastructureItem, ConnectedInfrastructureSurface } from '@/types/api';
import { getAgentCapabilityLabel, type AgentCapability } from '@/utils/agentCapabilityPresentation';
import { getSourcePlatformLabel } from '@/utils/sourcePlatforms';
import { type PlatformConnectionsView } from './platformConnectionsModel';
import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel';
export const TOKEN_PLACEHOLDER = '<api-token>';
export type PlatformConnectionsView = 'proxmox' | 'truenas' | 'vmware';
export type AgentPlatform = 'linux' | 'macos' | 'freebsd' | 'windows';
export type UnifiedAgentStatus = 'active' | 'removed';
export type ScopeCategory = 'default' | 'profile' | 'ai-managed' | 'na';
@@ -15,16 +15,6 @@ export type InfrastructurePanelStep = 'pick' | InfrastructureAddStep;
const INFRASTRUCTURE_BASE_PATH = '/settings/infrastructure';
export const INFRASTRUCTURE_ADD_QUERY_PARAM = 'add';
const LEGACY_INSTALL_PATH = `${INFRASTRUCTURE_BASE_PATH}/install`;
const LEGACY_PLATFORMS_PATH = `${INFRASTRUCTURE_BASE_PATH}/platforms`;
function matchesExactPath(pathname: string, expected: string): boolean {
return pathname === expected || pathname === `${expected}/`;
}
function matchesPathPrefix(pathname: string, expected: string): boolean {
return pathname === expected || pathname.startsWith(`${expected}/`);
}
export function normalizeInfrastructurePanelStep(
value: string | null | undefined,
@@ -59,12 +49,6 @@ export function buildInfrastructureOnboardingPath(step: InfrastructurePanelStep
return `${INFRASTRUCTURE_BASE_PATH}?${params.toString()}`;
}
export function deriveAddStepFromLegacyPath(pathname: string): InfrastructurePanelStep | null {
if (matchesPathPrefix(pathname, LEGACY_INSTALL_PATH)) return 'linux-host';
if (matchesExactPath(pathname, LEGACY_PLATFORMS_PATH)) return 'pick';
return null;
}
export function deriveAddStepFromSearch(search: string): InfrastructurePanelStep | null {
const params = new URLSearchParams(search);
return normalizeInfrastructurePanelStep(params.get(INFRASTRUCTURE_ADD_QUERY_PARAM));
@@ -74,15 +58,9 @@ export function deriveAddStepFromLocation(
pathname: string,
search: string,
): InfrastructurePanelStep | null {
if (
pathname === INFRASTRUCTURE_BASE_PATH ||
pathname.startsWith(`${INFRASTRUCTURE_BASE_PATH}/`)
) {
const stepFromQuery = deriveAddStepFromSearch(search);
if (stepFromQuery) {
return stepFromQuery;
}
if (pathname !== INFRASTRUCTURE_BASE_PATH && pathname !== `${INFRASTRUCTURE_BASE_PATH}/`) {
return null;
}
return deriveAddStepFromLegacyPath(pathname);
return deriveAddStepFromSearch(search);
}
@@ -1,66 +0,0 @@
export type PlatformConnectionsView = 'proxmox' | 'truenas' | 'vmware';
export interface PlatformConnectionsTabDefinition {
id: PlatformConnectionsView;
label: string;
path: string;
}
export const PLATFORM_CONNECTIONS_PREFIX = '/settings/infrastructure/platforms';
const PLATFORM_CONNECTIONS_PROXMOX_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/proxmox`;
const PLATFORM_CONNECTIONS_TRUENAS_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/truenas`;
const PLATFORM_CONNECTIONS_VMWARE_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/vmware`;
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';
export const PLATFORM_CONNECTIONS_TABS: readonly PlatformConnectionsTabDefinition[] = [
{
id: 'proxmox',
label: 'Proxmox',
path: PLATFORM_CONNECTIONS_PROXMOX_PREFIX,
},
{
id: 'truenas',
label: 'TrueNAS',
path: PLATFORM_CONNECTIONS_TRUENAS_PREFIX,
},
{
id: 'vmware',
label: 'VMware',
path: PLATFORM_CONNECTIONS_VMWARE_PREFIX,
},
];
export function getPlatformConnectionsViewFromPath(pathname: string): PlatformConnectionsView {
if (pathname.startsWith(PLATFORM_CONNECTIONS_VMWARE_PREFIX)) {
return 'vmware';
}
if (pathname.startsWith(PLATFORM_CONNECTIONS_TRUENAS_PREFIX)) {
return 'truenas';
}
if (pathname.startsWith(LEGACY_VMWARE_PREFIX)) {
return 'vmware';
}
if (pathname.startsWith(LEGACY_TRUENAS_PREFIX)) {
return 'truenas';
}
if (pathname.startsWith(PLATFORM_CONNECTIONS_PREFIX)) {
return 'proxmox';
}
if (
pathname.startsWith(LEGACY_PROXMOX_PREFIX) ||
pathname.startsWith(LEGACY_PROXMOX_API_PREFIX)
) {
return 'proxmox';
}
return 'proxmox';
}
export function buildPlatformConnectionsPath(view: PlatformConnectionsView): string {
return (
PLATFORM_CONNECTIONS_TABS.find((tab) => tab.id === view)?.path ??
PLATFORM_CONNECTIONS_PROXMOX_PREFIX
);
}
@@ -86,15 +86,11 @@ export type SettingsHeaderMetaMap = Record<SettingsTab, SettingsHeaderMeta>;
// Default landing tab for /settings when no deep-link tab is provided.
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 RETIRED_SETTINGS_WORKLOADS_PREFIX = '/settings/workloads';
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 PROXMOX_PREFIX = `${INFRASTRUCTURE_SYSTEMS_PREFIX}/platforms/proxmox`;
const LEGACY_PROXMOX_PREFIX = '/settings/infrastructure/proxmox';
const LEGACY_PROXMOX_API_PREFIX = '/settings/infrastructure/api';
const LEGACY_INTEGRATIONS_API_PREFIX = '/settings/integrations/api';
const LEGACY_SETTINGS_OPERATIONS_PREFIX = '/settings/operations';
const SECURITY_API_PREFIX = '/settings/security/api';
@@ -104,26 +100,22 @@ const LEGACY_SYSTEM_PRO_PREFIX = '/settings/system-pro';
const PROXMOX_AGENT_META: Record<
AgentKey,
{
path: string;
platformType: ProxmoxPlatformType;
label: string;
nodeLabel: string;
}
> = {
pve: {
path: `${PROXMOX_PREFIX}/pve`,
platformType: 'proxmox-pve',
label: 'Proxmox VE',
nodeLabel: 'Proxmox VE node',
},
pbs: {
path: `${PROXMOX_PREFIX}/pbs`,
platformType: 'proxmox-pbs',
label: 'Proxmox Backup Server',
nodeLabel: 'Proxmox Backup Server',
},
pmg: {
path: `${PROXMOX_PREFIX}/pmg`,
platformType: 'proxmox-pmg',
label: 'Proxmox Mail Gateway',
nodeLabel: 'Proxmox Mail Gateway',
@@ -139,22 +131,22 @@ const normalizeSettingsPath = (path: string): string => {
return trimmed;
};
export function isRetiredSettingsCompatibilityPath(path: string): boolean {
const normalizedPath = normalizeSettingsPath(path);
return (
normalizedPath === RETIRED_SETTINGS_WORKLOADS_PREFIX ||
normalizedPath.startsWith(`${RETIRED_SETTINGS_WORKLOADS_PREFIX}/`) ||
normalizedPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`)
);
}
export function resolveCanonicalSettingsPath(path: string): string | null {
const normalizedPath = normalizeSettingsPath(path);
if (!normalizedPath.startsWith('/settings')) return null;
if (normalizedPath !== '/settings' && !normalizedPath.startsWith('/settings/')) return null;
if (isRetiredSettingsCompatibilityPath(normalizedPath)) return null;
if (normalizedPath === '/settings') {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
if (normalizedPath === LEGACY_AGENTS_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
if (normalizedPath === LEGACY_DOCKER_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
// All infrastructure sub-paths collapse to the single infrastructure workspace.
if (normalizedPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`)) {
return INFRASTRUCTURE_SYSTEMS_PREFIX;
}
if (normalizedPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
return settingsTabPath(DEFAULT_SETTINGS_TAB);
}
@@ -167,14 +159,6 @@ export function resolveCanonicalSettingsPath(path: string): string | null {
if (normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/`)) {
return buildLegacyOperationsSettingsPath(normalizedPath);
}
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;
}
@@ -191,12 +175,7 @@ export function deriveTabFromPath(path: string): SettingsTab {
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
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
) {
if (canonicalPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
return 'infrastructure-systems';
}
@@ -206,7 +185,10 @@ export function deriveTabFromPath(path: string): SettingsTab {
if (canonicalPath.includes('/settings/system-recovery')) return 'system-recovery';
if (canonicalPath.includes('/settings/system-ai')) return 'system-ai';
if (canonicalPath.includes('/settings/system-relay')) return 'system-relay';
if (canonicalPath.includes(SYSTEM_BILLING_PREFIX) || canonicalPath.includes(LEGACY_SYSTEM_PRO_PREFIX))
if (
canonicalPath.includes(SYSTEM_BILLING_PREFIX) ||
canonicalPath.includes(LEGACY_SYSTEM_PRO_PREFIX)
)
return 'system-billing';
if (canonicalPath.startsWith(SUPPORT_LOGS_PREFIX)) return 'support-logs';
if (canonicalPath.startsWith(SUPPORT_REPORTING_PREFIX)) return 'support-reporting';
@@ -234,29 +216,6 @@ export function deriveTabFromPath(path: string): SettingsTab {
return DEFAULT_SETTINGS_TAB;
}
export function deriveAgentFromPath(path: string): AgentKey | null {
const normalizedPath = normalizeSettingsPath(path);
for (const [agent, meta] of Object.entries(PROXMOX_AGENT_META) as Array<
[AgentKey, (typeof PROXMOX_AGENT_META)[AgentKey]]
>) {
if (normalizedPath.includes(meta.path)) return agent;
}
return null;
}
export function isProxmoxSettingsPath(path: string): boolean {
const normalizedPath = normalizeSettingsPath(path);
return (
normalizedPath.startsWith(PROXMOX_PREFIX) ||
normalizedPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
normalizedPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
);
}
export function settingsAgentPath(agent: AgentKey): string {
return PROXMOX_AGENT_META[agent].path;
}
export function settingsAgentPlatformType(agent: AgentKey): ProxmoxPlatformType {
return PROXMOX_AGENT_META[agent].platformType;
}
@@ -290,13 +249,6 @@ export function deriveTabFromQuery(search: string): SettingsTab | null {
switch (tab) {
case 'infrastructure':
case 'workloads':
case 'agents':
return 'infrastructure-systems';
case 'connections':
case 'platforms':
case 'proxmox':
case 'install':
return 'infrastructure-systems';
case 'system-recovery':
return 'system-recovery';
@@ -324,8 +276,6 @@ export function deriveTabFromQuery(search: string): SettingsTab | null {
return 'support-logs';
case 'api':
return 'api';
case 'docker':
return 'infrastructure-systems';
case 'organization-overview':
return 'organization-overview';
case 'organization-access':
@@ -392,6 +342,44 @@ export function settingsTabPath(tab: SettingsTab): string {
}
}
const ROUTEABLE_SETTINGS_PATHS = new Set<string>([
settingsTabPath('infrastructure-systems'),
settingsTabPath('system-general'),
settingsTabPath('system-network'),
settingsTabPath('system-updates'),
settingsTabPath('system-recovery'),
settingsTabPath('system-ai'),
settingsTabPath('system-relay'),
settingsTabPath('system-billing'),
`${SYSTEM_BILLING_PREFIX}/usage`,
settingsTabPath('support-diagnostics'),
settingsTabPath('support-reporting'),
settingsTabPath('support-logs'),
settingsTabPath('organization-overview'),
settingsTabPath('organization-access'),
settingsTabPath('organization-billing'),
settingsTabPath('organization-billing-admin'),
settingsTabPath('organization-sharing'),
settingsTabPath('api'),
settingsTabPath('security-overview'),
settingsTabPath('security-data-handling'),
settingsTabPath('security-auth'),
settingsTabPath('security-sso'),
settingsTabPath('security-roles'),
settingsTabPath('security-users'),
settingsTabPath('security-audit'),
settingsTabPath('security-webhooks'),
]);
export function isRouteableSettingsPath(path: string): boolean {
const normalizedPath = normalizeSettingsPath(path);
if (normalizedPath === '/settings') return true;
if (isRetiredSettingsCompatibilityPath(normalizedPath)) return false;
const canonicalPath = resolveCanonicalSettingsPath(normalizedPath);
return canonicalPath ? ROUTEABLE_SETTINGS_PATHS.has(canonicalPath) : false;
}
export function isInfrastructureSettingsTab(tab: SettingsTab): tab is InfrastructureSettingsTab {
return tab === 'infrastructure-systems';
}
@@ -1,13 +1,13 @@
export {
DEFAULT_SETTINGS_TAB,
agentKeyFromPlatformType,
deriveAgentFromPath,
deriveTabFromPath,
deriveTabFromQuery,
isRetiredSettingsCompatibilityPath,
isRouteableSettingsPath,
resolveCanonicalSettingsPath,
settingsAgentLabel,
settingsAgentNodeLabel,
settingsAgentPath,
settingsAgentPlatformType,
settingsTabPath,
type AgentKey,
@@ -4,17 +4,11 @@ import {
sessionPresentationPolicyResolved,
} from '@/stores/sessionPresentationPolicy';
import { resolveCanonicalSelfHostedBillingHref } from '@/utils/pricingHandoff';
import {
buildInfrastructureOnboardingPath,
buildInfrastructureWorkspacePath,
deriveAddStepFromLegacyPath,
} from './infrastructureWorkspaceModel';
import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel';
import {
DEFAULT_SETTINGS_TAB,
deriveAgentFromPath,
deriveTabFromPath,
deriveTabFromQuery,
isProxmoxSettingsPath,
resolveCanonicalSettingsPath,
settingsTabPath,
type AgentKey,
@@ -96,9 +90,7 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
if (
sessionPresentationPolicyResolved() &&
presentationPolicyIsReadOnly() &&
(path.startsWith('/settings/infrastructure') ||
path === '/settings/workloads' ||
path === '/settings/workloads/docker')
path.startsWith('/settings/infrastructure')
) {
navigate(buildInfrastructureWorkspacePath(), {
replace: true,
@@ -107,24 +99,6 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
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 infrastructureOnboardingStep = deriveAddStepFromLegacyPath(path);
if (infrastructureOnboardingStep) {
navigate(buildInfrastructureOnboardingPath(infrastructureOnboardingStep), {
replace: true,
scroll: false,
});
return;
}
const canonicalPath = resolveCanonicalSettingsPath(path);
if (canonicalPath && canonicalPath !== path) {
navigate(canonicalPath, {
@@ -146,7 +146,7 @@ describe('MobileNavBar', () => {
id: 'proxmox',
label: 'Proxmox',
route: '/proxmox/overview',
settingsRoute: '/settings/infrastructure/platforms/proxmox/pve',
settingsRoute: '/settings/infrastructure',
tooltip: 'Proxmox',
enabled: true,
live: true,
@@ -951,9 +951,7 @@ describe('frontend resource type boundaries', () => {
expect(tagBadgesSource).toContain("from '@/components/shared/Tooltip'");
expect(resourceDetailSummarySource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerOverviewSource).toContain('getAllFilterOptionLabel');
expect(resourceDetailSummarySource).not.toContain(
"from '@/components/Workloads/TagBadges'",
);
expect(resourceDetailSummarySource).not.toContain("from '@/components/Workloads/TagBadges'");
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerOverviewTab'");
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerDebugTab'");
expect(resourceDetailDrawerStateSource).toContain(
@@ -3317,7 +3315,7 @@ describe('frontend resource type boundaries', () => {
'@/components/Settings/infrastructureWorkspaceModel'.replace('@/components/Settings/', './'),
);
expect(infrastructureWorkspaceSource).not.toContain('inferViewFromPath');
expect(infrastructureWorkspaceModelSource).toContain(
expect(infrastructureWorkspaceModelSource).not.toContain(
'export function deriveAddStepFromLegacyPath',
);
expect(infrastructureWorkspaceModelSource).toContain(