From 4e4fcb5dbe5a06a642e551e8c5b1c05c307ec9e4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Apr 2026 20:03:54 +0100 Subject: [PATCH] Move operations tools into settings support --- ARCHITECTURE.md | 5 +- .../v6/internal/subsystems/agent-lifecycle.md | 3 + .../v6/internal/subsystems/ai-runtime.md | 9 +- .../v6/internal/subsystems/cloud-paid.md | 6 +- .../subsystems/frontend-primitives.md | 160 +++++++++--------- .../subsystems/performance-and-scalability.md | 4 + .../v6/internal/subsystems/registry.json | 6 +- .../internal/subsystems/storage-recovery.md | 4 + frontend-modern/src/App.tsx | 8 - frontend-modern/src/AppLayout.tsx | 21 +-- .../src/__tests__/App.architecture.test.ts | 8 +- .../monitoredSystemModelGuardrails.test.ts | 2 +- .../__tests__/settingsArchitecture.test.ts | 4 +- .../settingsNavigation.integration.test.tsx | 25 +++ .../__tests__/settingsRouting.test.ts | 28 +++ .../__tests__/useSettingsShellState.test.ts | 2 +- .../components/Settings/settingsHeaderMeta.ts | 14 +- .../components/Settings/settingsNavCatalog.ts | 31 +++- .../Settings/settingsNavVisibility.ts | 11 ++ .../Settings/settingsNavigationModel.ts | 68 +++++++- .../Settings/settingsPanelRegistry.ts | 9 + .../Settings/settingsPanelRegistryLoaders.ts | 9 + .../components/Settings/useSettingsAccess.ts | 12 ++ .../Settings/useSettingsShellState.ts | 2 +- .../SetupWizard/SetupCompletionPanel.tsx | 4 +- .../SetupCompletionPanel.guardrails.test.ts | 2 +- .../shared/__tests__/MobileNavBar.test.tsx | 2 + .../components/shared/mobileNavBarModel.ts | 4 +- .../operations/OperationsPageSurface.tsx | 101 ----------- .../OperationsPageSurface.demoMode.test.tsx | 74 -------- .../operations/operationsPageModel.ts | 40 ----- frontend-modern/src/pages/Operations.tsx | 10 +- .../__tests__/Operations.helpers.test.ts | 38 ++--- .../src/routing/__tests__/navigation.test.ts | 6 +- frontend-modern/src/routing/navigation.ts | 5 +- frontend-modern/src/routing/routePreload.ts | 7 - .../frontendResourceTypeBoundaries.test.ts | 23 +-- 37 files changed, 364 insertions(+), 403 deletions(-) delete mode 100644 frontend-modern/src/features/operations/OperationsPageSurface.tsx delete mode 100644 frontend-modern/src/features/operations/__tests__/OperationsPageSurface.demoMode.test.tsx delete mode 100644 frontend-modern/src/features/operations/operationsPageModel.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3720d6534..c8dc2476c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -140,10 +140,9 @@ Navigation is organised by **task**, not by platform: | `/dashboard` | Dashboard | Summary panels and metrics | | `/alerts/*` | Alerts | Alert rules, active alerts, history | | `/ai/*` | AI Intelligence | Patrol findings, investigations, forecasts | -| `/settings/*` | Settings | Configuration, security, AI, relay | -| `/operations/*` | Operations | Operational tools | +| `/settings/*` | Settings | Configuration, security, diagnostics, reporting, AI, relay | -Legacy route aliases have been removed; canonical v6 routes are the only supported navigation surface. +Canonical v6 task surfaces live on the routes above; legacy aliases redirect into those canonical settings and patrol paths. ### State Management - **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching. diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 52ac4ec90..6adcd751a 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -397,6 +397,9 @@ an add-only capacity posture. install the first host again. While the first host is still pending, that same completion narrative must describe Infrastructure Install as the place where the first-host scoped install token is prepared from setup handoff, + and when it names the shared settings workspace for follow-up lifecycle + control it must use the canonical `Connections & Inventory` label instead + of reviving the retired `Infrastructure Operations` wording. not as a second manual token-generation task the operator still needs to figure out. 10. Keep API-backed platform onboarding explicit across diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 995355d81..a6c37f91b 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -249,10 +249,11 @@ organization chrome: `frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` may hide org switchers or demo-only org labels, but they must not couple assistant visibility, session reset, or drawer-open behavior to that organization presentation state. -That same shell boundary also owns demo-only Operations suppression: -`frontend-modern/src/AppLayout.tsx` may remove the top-level Operations route -from the public demo shell, but assistant availability and reset behavior must -stay independent of that utility-tab presentation choice. +That same shell boundary also owns demo-only support-surface suppression: +Pulse no longer exposes Operations as a top-level route. Demo-only support +surfaces now hide inside the shared Settings navigation instead, and assistant +availability plus reset behavior must stay independent of that settings-nav +presentation choice. Authenticated `/login` recovery belongs to that same route shell boundary: once login succeeds, `frontend-modern/src/App.tsx` must resolve `/login` through the canonical post-auth landing route instead of leaving the diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 54104d71d..c25026141 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -305,8 +305,10 @@ commercial posture store and billing-entitlements store must also fail closed locally until the shared presentation policy resolves, then stay fail-closed in demo mode so hidden routes are not probed from the browser shell. That same browser-shell boundary also owns utility-nav suppression: -`frontend-modern/src/AppLayout.tsx` must drop the top-level Operations tab in -public demo mode instead of leaving diagnostics or system-log shells +`frontend-modern/src/AppLayout.tsx` must not expose a separate top-level +Operations destination. Diagnostics, reports, and logs now live under the +canonical Settings support navigation, and public demo mode must keep those +support-only entries hidden instead of leaving the retired operations surface discoverable after commercial surfaces are hidden. Deep-linkable commercial panels must consume the same resolved presentation policy directly, not rely only on settings navigation to keep public-demo diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index cb7378e61..66f8d732e 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -93,61 +93,59 @@ work extends shared components instead of creating new local variants. 67. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts` 68. `frontend-modern/src/utils/systemLogsPresentation.ts` 69. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx` -70. `frontend-modern/src/features/operations/OperationsPageSurface.tsx` -71. `frontend-modern/src/features/operations/operationsPageModel.ts` -72. `frontend-modern/src/pages/Operations.tsx` -73. `frontend-modern/src/components/Settings/ResourcePicker.tsx` -74. `frontend-modern/src/components/Settings/reportingResourceTypes.ts` -75. `frontend-modern/src/utils/reportableResourceTypes.ts` -76. `frontend-modern/src/utils/reportingResourceTypes.ts` -77. `frontend-modern/src/utils/problemResourcePresentation.ts` -78. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts` -79. `frontend-modern/src/utils/dashboardGuestPresentation.ts` -80. `frontend-modern/src/utils/dashboardKpiPresentation.ts` -81. `frontend-modern/src/utils/dashboardTrendPresentation.ts` -82. `frontend-modern/src/components/Toast/Toast.tsx` -83. `frontend-modern/src/utils/toast.ts` -84. `frontend-modern/src/utils/semanticTonePresentation.ts` -85. `frontend-modern/src/utils/emptyStatePresentation.ts` -86. `frontend-modern/src/utils/typeColumnPresentation.ts` -87. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts` -88. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx` -89. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` -90. `frontend-modern/src/components/Settings/networkSettingsModel.ts` -91. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` -92. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` -93. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` -94. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` -95. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` -96. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` -97. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` -98. `frontend-modern/src/components/Settings/settingsRouting.ts` -99. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` -100. `frontend-modern/src/components/Settings/settingsTypes.ts` -101. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` -102. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` -103. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` -104. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` -105. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` -106. `frontend-modern/src/utils/environmentLockPresentation.ts` -107. `frontend-modern/src/utils/docsLinks.ts` -108. `tests/integration/tests/20-local-doc-links.spec.ts` -109. `frontend-modern/src/index.css` -110. `frontend-modern/src/components/shared/summaryInteractionA11y.ts` -111. `frontend-modern/src/components/shared/SummaryRowActionButton.tsx` -112. `frontend-modern/src/hooks/createNonSuspendingQuery.ts` -113. `frontend-modern/src/components/shared/SummaryTableCardHeader.tsx` -114. `frontend-modern/src/components/shared/UpgradeLink.tsx` -115. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` -116. `frontend-modern/src/utils/upgradeNavigation.ts` -117. `frontend-modern/src/components/DemoBanner.tsx` -118. `frontend-modern/src/components/Login.tsx` -119. `frontend-modern/src/stores/demoMode.ts` -120. `frontend-modern/src/stores/sessionCapabilities.ts` -121. `frontend-modern/src/stores/sessionPresentationPolicy.ts` -122. `frontend-modern/src/stores/licenseCommercial.ts` -123. `frontend-modern/src/useAppRuntimeState.ts` -124. `frontend-modern/src/stores/aiChat.ts` +70. `frontend-modern/src/pages/Operations.tsx` +71. `frontend-modern/src/components/Settings/ResourcePicker.tsx` +72. `frontend-modern/src/components/Settings/reportingResourceTypes.ts` +73. `frontend-modern/src/utils/reportableResourceTypes.ts` +74. `frontend-modern/src/utils/reportingResourceTypes.ts` +75. `frontend-modern/src/utils/problemResourcePresentation.ts` +76. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts` +77. `frontend-modern/src/utils/dashboardGuestPresentation.ts` +78. `frontend-modern/src/utils/dashboardKpiPresentation.ts` +79. `frontend-modern/src/utils/dashboardTrendPresentation.ts` +80. `frontend-modern/src/components/Toast/Toast.tsx` +81. `frontend-modern/src/utils/toast.ts` +82. `frontend-modern/src/utils/semanticTonePresentation.ts` +83. `frontend-modern/src/utils/emptyStatePresentation.ts` +84. `frontend-modern/src/utils/typeColumnPresentation.ts` +85. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts` +86. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx` +87. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` +88. `frontend-modern/src/components/Settings/networkSettingsModel.ts` +89. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` +90. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` +91. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` +92. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` +93. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` +94. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` +95. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` +96. `frontend-modern/src/components/Settings/settingsRouting.ts` +97. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` +98. `frontend-modern/src/components/Settings/settingsTypes.ts` +99. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` +100. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` +101. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` +102. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` +103. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` +104. `frontend-modern/src/utils/environmentLockPresentation.ts` +105. `frontend-modern/src/utils/docsLinks.ts` +106. `tests/integration/tests/20-local-doc-links.spec.ts` +107. `frontend-modern/src/index.css` +108. `frontend-modern/src/components/shared/summaryInteractionA11y.ts` +109. `frontend-modern/src/components/shared/SummaryRowActionButton.tsx` +110. `frontend-modern/src/hooks/createNonSuspendingQuery.ts` +111. `frontend-modern/src/components/shared/SummaryTableCardHeader.tsx` +112. `frontend-modern/src/components/shared/UpgradeLink.tsx` +113. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` +114. `frontend-modern/src/utils/upgradeNavigation.ts` +115. `frontend-modern/src/components/DemoBanner.tsx` +116. `frontend-modern/src/components/Login.tsx` +117. `frontend-modern/src/stores/demoMode.ts` +118. `frontend-modern/src/stores/sessionCapabilities.ts` +119. `frontend-modern/src/stores/sessionPresentationPolicy.ts` +120. `frontend-modern/src/stores/licenseCommercial.ts` +121. `frontend-modern/src/useAppRuntimeState.ts` +122. `frontend-modern/src/stores/aiChat.ts` ## Shared Boundaries @@ -185,11 +183,12 @@ work extends shared components instead of creating new local variants. posture: `/alerts` may continue exposing reporting tabs such as overview and history, but activation controls plus configuration routes must collapse out of the public-demo shell instead of advertising blocked management actions. - That same public-demo presentation boundary also owns top-level Operations - posture: the authenticated demo shell must not advertise the Operations - utility tab, and `/operations` deep links must hand back to the dashboard - instead of surfacing diagnostics or system-log chrome that the backend hides - for demo sessions. + That same public-demo presentation boundary also owns Settings support + posture: the authenticated demo shell must not advertise `Diagnostics & + Health`, `Data & Reports`, or `System Logs` in the Settings navigation, and + legacy `/operations/*` links must resolve through the canonical Settings + routing boundary instead of reviving a standalone Operations utility tab or + route-local support shell. Shared sparkline primitives must also stay CSP-safe by construction: `frontend-modern/src/components/shared/InteractiveSparkline.tsx` may use SVG attributes and shared state/model helpers for cursor, axis-label, and @@ -456,10 +455,11 @@ connections` visible as the API-backed alternative for Proxmox and top-level page-header contract before publication. The audit may follow local imports when a route shell composes `PageHeader` through a nested surface, and settings coverage must stay limited to top-level registry - panels rather than every helper `*Panel.tsx` file. Route shells such as - `frontend-modern/src/features/operations/OperationsPageSurface.tsx` must - therefore keep the shared `PageHeader` above owned subtabs instead of - drifting back to page-local `

` framing. + panels rather than every helper `*Panel.tsx` file. The canonical Settings + shell therefore owns the shared `PageHeader` for support tools, and + `frontend-modern/src/pages/Operations.tsx` must stay a redirect-only + compatibility handoff instead of regrowing a second route-local heading, + tab strip, or page shell for diagnostics, reporting, or logs. 23. Keep the authenticated app root aligned with that same first-session path. That same shared-primitive ownership now includes contextual row focus. `frontend-modern/src/components/shared/contextualFocus.ts` is the canonical @@ -1138,7 +1138,10 @@ handoff lifecycle, and `frontend-modern/src/components/shared/mobileNavBarModel.ts` owns platform and utility tab ordering, alert badge counts, fade-state derivation, and tab button class policy. Future mobile-nav work should extend those owners instead -of pushing tab-order or DOM lifecycle logic back into the shared shell. +of pushing tab-order or DOM lifecycle logic back into the shared shell. With +support/admin tools moved under Settings, that utility ordering must no longer +reserve a standalone `operations` slot; alerts, Patrol, and Settings are the +remaining authenticated utility tabs. The shared command palette now follows that same owner split. `frontend-modern/src/components/shared/CommandPaletteModal.tsx` stays the render shell, `frontend-modern/src/components/shared/useCommandPaletteState.ts` @@ -1302,17 +1305,14 @@ must consume the direct Proxmox panel contract through registry stays a shell/composition owner and does not depend on `ProxmoxSettingsPanel.tsx` as though the panel still owned the runtime model. -The operations route now follows the same thin-route pattern as infrastructure, -storage, and Patrol. `frontend-modern/src/pages/Operations.tsx` stays the route -shell, `frontend-modern/src/features/operations/OperationsPageSurface.tsx` owns -the tabbed operations surface, and -`frontend-modern/src/features/operations/operationsPageModel.ts` owns the tab -and path contract. The operations route must keep its navigation routed through -the shared `frontend-modern/src/components/shared/Subtabs.tsx` primitive rather -than rebuilding a bespoke page-local tab bar. When the session presentation -policy marks the operator as a public demo viewer, that same route owner must -suppress the surface entirely and hand the browser back to the canonical -dashboard route instead of rendering diagnostics, reporting, or logs shells +The retired `/operations` route is now a thin compatibility redirect only. +`frontend-modern/src/pages/Operations.tsx` may normalize legacy `/operations/*` +links into the canonical Settings support routes, but diagnostics, reports, +and logs now belong to the shared Settings shell instead of a bespoke page- +local tab surface. Support-only navigation must therefore route through the +shared settings owners rather than rebuilding a second route-level shell, and +public demo posture must keep those support entries hidden from the Settings +navigation instead of reviving a standalone operations page. that are unavailable in demo mode. The dashboard overview route now follows that same feature-owner pattern for @@ -1586,6 +1586,12 @@ final memoized registry composition only. `frontend-modern/src/components/Settin must stay a shell that wires those owners together instead of re-accumulating infrastructure workspace props, registry context maps, system panel prop maps, lazy loader definitions, or discovery draft state inline. +That same settings-routing contract now also owns the Support group for +`Diagnostics & Health`, `Data & Reports`, and `System Logs`: the navigation +model must normalize both `/settings/operations/*` and legacy `/operations/*` +compatibility links into `/settings/support/*`, and the catalog plus visibility +owners must treat those support surfaces as Settings-native pages rather than +as a second top-level utility destination. The resource incident panel's collapsed activity summary is now part of that same shared primitive boundary. Event-type count chips, visible-event copy, @@ -2119,7 +2125,7 @@ reference cases, and locks that direct-root contract so single-surface pages do not quietly regain redundant outer spacing chrome. The same shared settings-shell boundary now also owns the API-backed -alternative path inside Infrastructure Operations. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, +alternative path inside Connections & Inventory. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, `frontend-modern/src/components/Settings/settingsNavigationModel.ts`, `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`, `frontend-modern/src/utils/infrastructureEmptyStatePresentation.ts`, and adjacent setup guidance must diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 07e2695a6..015a24011 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -228,6 +228,10 @@ regression protection. is canonical, that alias must stay a thin redirect and must not mount a second Patrol shell or duplicate app bootstrap work before navigation settles on the canonical route. + The same rule now applies to the retired `/operations` surface: legacy + `/operations/*` links may redirect into Settings support routes, but they + must not mount a second diagnostics/reporting shell or pay extra bootstrap + work before the canonical Settings URL takes over. Authenticated `/login` recovery belongs to that same app-shell boundary: `frontend-modern/src/App.tsx` must redirect that route back to the canonical dashboard landing path instead of leaving the freshly diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 0dabdc6d0..8d30a1725 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2769,8 +2769,6 @@ "frontend-modern/src/features/dashboardOverview/KPIStrip.tsx", "frontend-modern/src/features/dashboardOverview/ProblemResourcesTable.tsx", "frontend-modern/src/features/dashboardOverview/TrendCharts.tsx", - "frontend-modern/src/features/operations/operationsPageModel.ts", - "frontend-modern/src/features/operations/OperationsPageSurface.tsx", "frontend-modern/src/hooks/createNonSuspendingQuery.ts", "frontend-modern/src/pages/Operations.tsx", "frontend-modern/src/stores/aiChat.ts", @@ -2928,7 +2926,7 @@ }, { "id": "route-shell-and-operations", - "label": "route shell and operations proof", + "label": "route shell and settings support proof", "match_prefixes": [], "match_files": [ "frontend-modern/src/components/Settings/ReportingPanel.tsx", @@ -2938,8 +2936,6 @@ "frontend-modern/src/components/Settings/SystemLogsPanel.tsx", "frontend-modern/src/components/Settings/useReportingPanelState.ts", "frontend-modern/src/components/Settings/useSystemLogsPanelState.ts", - "frontend-modern/src/features/operations/operationsPageModel.ts", - "frontend-modern/src/features/operations/OperationsPageSurface.tsx", "frontend-modern/src/pages/Operations.tsx", "frontend-modern/src/utils/reportableResourceTypes.ts", "frontend-modern/src/utils/reportingPresentation.ts", diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 1bc6f35e4..7e6a9ffc1 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -277,6 +277,10 @@ querying, and the operator-facing storage health presentation layer. may hide top-bar org chrome for public demo posture, but it must not leak into storage/recovery preview route ownership, first-session recovery copy, or route-level framing decisions. + Legacy `/operations/*` entrypoints now redirect into Settings support + surfaces. That compatibility path must stay a thin redirect in + `frontend-modern/src/App.tsx` and must not grow a second authenticated + shell boundary that competes with storage/recovery route ownership. That same shared app-shell boundary must also respect blocking shared dialogs: background assistant affordances may hide while a modal owns the viewport, but storage/recovery routes must not grow their own parallel diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 4843a59ac..79de48e71 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -95,7 +95,6 @@ const APP_SHELL_ROUTE_PRELOAD_PATHS = [ ROOT_PATROL_PATH, '/alerts', STORAGE_PATH, - '/operations', '/settings', ] as const; @@ -203,12 +202,6 @@ function GlobalUpdateProgressWatcher() { } function App() { - const LegacyOperationsSettingsRedirect = () => { - const location = useLocation(); - const canonicalPath = - location.pathname.replace(/^\/settings\/operations(?=\/|$)/, '/operations') || '/operations'; - return ; - }; const LegacyPatrolRouteRedirect = () => { const location = useLocation(); const canonicalPath = @@ -497,7 +490,6 @@ function App() { - diff --git a/frontend-modern/src/AppLayout.tsx b/frontend-modern/src/AppLayout.tsx index 3b3684499..4734d4b76 100644 --- a/frontend-modern/src/AppLayout.tsx +++ b/frontend-modern/src/AppLayout.tsx @@ -18,7 +18,6 @@ import BellIcon from 'lucide-solid/icons/bell'; import SettingsIcon from 'lucide-solid/icons/settings'; import Maximize2Icon from 'lucide-solid/icons/maximize-2'; import Minimize2Icon from 'lucide-solid/icons/minimize-2'; -import ActivityIcon from 'lucide-solid/icons/activity'; import { MobileNavBar } from '@/components/shared/MobileNavBar'; import { ReleaseCandidateBanner } from '@/components/shared/ReleaseCandidateBanner'; import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState'; @@ -43,10 +42,7 @@ import { getKioskModePreference, setKioskMode } from '@/utils/url'; import { updateStore } from '@/stores/updates'; import { aiChatStore } from '@/stores/aiChat'; import { isPro } from '@/stores/licenseCommercial'; -import { - presentationPolicyHidesUpgradePrompts, - presentationPolicyIsDemoMode, -} from '@/stores/sessionPresentationPolicy'; +import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { AI_CHAT_LAUNCHER_ARIA_LABEL, getAIChatLauncherTitle, @@ -70,7 +66,7 @@ type PlatformTab = { }; type UtilityTab = { - id: 'alerts' | 'ai' | 'operations' | 'settings'; + id: 'alerts' | 'ai' | 'settings'; label: string; route: string; tooltip: string; @@ -434,19 +430,6 @@ export function AppLayout(props: AppLayoutProps) { }, ]; - if (!presentationPolicyIsDemoMode()) { - tabs.push({ - id: 'operations', - label: 'Operations', - route: '/operations', - tooltip: 'System operations, diagnostics, and reporting', - badge: null, - count: undefined, - breakdown: undefined, - icon: , - }); - } - if (hasSettingsAccess) { tabs.push({ id: 'settings', diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index d8a2467b8..b0d1d5269 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -84,8 +84,11 @@ describe('App architecture', () => { expect(appLayoutSource).toContain( '', ); - expect(appLayoutSource).toContain("const blockedPrefixes = ['/settings', '/operations', '/patrol', '/ai'];"); + expect(appLayoutSource).toContain( + "const blockedPrefixes = ['/settings', '/operations', '/patrol', '/ai'];", + ); expect(appLayoutSource).toContain("route: '/patrol',"); + expect(appLayoutSource).not.toContain("route: '/operations',"); expect(appLayoutSource).not.toContain('props.connected()'); expect(appLayoutSource).toContain('const utilityTabs = createMemo(() =>'); expect(appLayoutSource).not.toContain("import { isMultiTenantEnabled } from '@/stores/license';"); @@ -95,8 +98,7 @@ describe('App architecture', () => { expect(appLayoutSource).not.toContain('sessionPresentationPolicyResolved'); expect(appLayoutSource).not.toContain('presentationPolicyHidesCommercialSurfaces'); expect(appLayoutSource).not.toContain('presentationPolicyHidesOrganizationSurfaces'); - expect(appLayoutSource).toContain('presentationPolicyIsDemoMode'); - expect(appLayoutSource).toContain("if (!presentationPolicyIsDemoMode()) {"); + expect(appLayoutSource).not.toContain('presentationPolicyIsDemoMode'); expect(appLayoutSource).toContain('await preloadRouteModule(targetRoute);'); expect(appLayoutSource).toContain('await preloadRouteModule(tab.route);'); expect(appLayoutSource).toContain('onMouseEnter={() => warmNavigationTarget('); diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index ecff8f093..5ee95fff5 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -992,7 +992,7 @@ describe('monitored-system model guardrails', () => { expect(setupCompletionPanelSource).toContain('Open Infrastructure Install'); expect(setupCompletionPanelSource).toContain('Open Platform connections'); expect(setupCompletionPanelSource).toContain( - 'The canonical install flow now lives in Infrastructure Operations.', + 'The canonical install flow now lives in Connections & Inventory.', ); expect(setupCompletionPanelSource).not.toContain('const hasAgentFacet = (resource: Resource)'); expect(setupCompletionPanelSource).not.toContain( diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 838e0c24a..897faa167 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -1430,7 +1430,7 @@ describe('Settings architecture guardrails', () => { } }); - it('keeps infrastructure shell framing focused on operations, not billing', () => { + it('keeps infrastructure shell framing focused on connected systems, not billing', () => { expect(settingsHeaderMetaSource).toContain('./selfHostedBillingPresentation'); expect(settingsHeaderMetaSource).toContain( 'SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral', @@ -1439,7 +1439,7 @@ describe('Settings architecture guardrails', () => { 'Billing and self-hosted plan features live in Pulse Pro.', ); expect(SETTINGS_HEADER_META['infrastructure-operations'].title).toBe( - 'Infrastructure Operations', + 'Connections & Inventory', ); expect(SETTINGS_HEADER_META['infrastructure-operations'].description).toContain( 'actively reporting', diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx index 603340582..8d92fc993 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -15,6 +15,9 @@ const canonicalTabPaths = { 'system-ai': '/settings/system-ai', 'system-relay': '/settings/system-relay', 'system-billing': '/settings/system/billing/plan', + 'support-diagnostics': '/settings/support/diagnostics', + 'support-reporting': '/settings/support/reporting', + 'support-logs': '/settings/support/logs', 'organization-overview': '/settings/organization', 'organization-access': '/settings/organization/access', 'organization-billing': '/settings/organization/billing', @@ -110,12 +113,34 @@ describe('settingsNavigation integration scaffold', () => { shouldHideSettingsNavItem('organization-billing', { hasFeature: hasFeatures(['multi_tenant']), runtimeCapabilitiesLoaded: () => true, + presentationPolicyIsDemoMode: true, presentationPolicyHidesCommercial: true, hostedModeEnabled: true, }), ).toBe(true); }); + it('hides support tabs in demo mode and before demo policy resolves', () => { + expect( + shouldHideSettingsNavItem('support-diagnostics', { + hasFeature: hasFeatures([]), + runtimeCapabilitiesLoaded: () => true, + presentationPolicyResolved: false, + hostedModeEnabled: false, + }), + ).toBe(true); + + expect( + shouldHideSettingsNavItem('support-reporting', { + hasFeature: hasFeatures([]), + runtimeCapabilitiesLoaded: () => true, + presentationPolicyResolved: true, + presentationPolicyIsDemoMode: true, + hostedModeEnabled: false, + }), + ).toBe(true); + }); + it('hides organization tabs in demo mode even when multi-tenant is enabled', () => { expect( shouldHideSettingsNavItem('organization-overview', { diff --git a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts index b927a4328..820a317a6 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + buildLegacyOperationsSettingsPath, agentKeyFromPlatformType, DEFAULT_SETTINGS_TAB, deriveAgentFromPath, @@ -25,6 +26,9 @@ const canonicalTabPaths = { 'system-ai': '/settings/system-ai', 'system-relay': '/settings/system-relay', 'system-billing': '/settings/system/billing/plan', + 'support-diagnostics': '/settings/support/diagnostics', + 'support-reporting': '/settings/support/reporting', + 'support-logs': '/settings/support/logs', 'organization-overview': '/settings/organization', 'organization-access': '/settings/organization/access', 'organization-billing': '/settings/organization/billing', @@ -68,6 +72,7 @@ describe('settingsNavigationModel', () => { expect(resolveCanonicalSettingsPath('/settings/workloads/docker')).toBe( '/settings/infrastructure/install', ); + expect(resolveCanonicalSettingsPath('/settings/support')).toBe('/settings/support/diagnostics'); expect(resolveCanonicalSettingsPath('/settings/system-updates')).toBe( '/settings/system-updates', ); @@ -92,6 +97,15 @@ describe('settingsNavigationModel', () => { expect(resolveCanonicalSettingsPath('/settings/integrations/api')).toBe( '/settings/security/api', ); + expect(resolveCanonicalSettingsPath('/settings/operations')).toBe( + '/settings/support/diagnostics', + ); + expect(resolveCanonicalSettingsPath('/settings/operations/reporting')).toBe( + '/settings/support/reporting', + ); + expect(resolveCanonicalSettingsPath('/settings/operations/logs')).toBe( + '/settings/support/logs', + ); expect(resolveCanonicalSettingsPath('/settings/system/billing')).toBe( '/settings/system/billing/plan', ); @@ -129,6 +143,9 @@ describe('settingsNavigationModel', () => { ['?tab=system-relay', 'system-relay'], ['?tab=system-pro', 'system-billing'], ['?tab=system-billing', 'system-billing'], + ['?tab=diagnostics', 'support-diagnostics'], + ['?tab=reporting', 'support-reporting'], + ['?tab=logs', 'support-logs'], ['?tab=system-recovery', 'system-recovery'], ['?tab=organization-overview', 'organization-overview'], ['?tab=organization-billing', 'organization-billing'], @@ -225,4 +242,15 @@ describe('settingsNavigationModel', () => { 'infrastructure-operations', ); }); + + it('maps support and legacy operations routes back to support tabs', () => { + expect(deriveTabFromPath('/settings/support/diagnostics')).toBe('support-diagnostics'); + expect(deriveTabFromPath('/settings/support/reporting')).toBe('support-reporting'); + expect(deriveTabFromPath('/settings/support/logs')).toBe('support-logs'); + expect(buildLegacyOperationsSettingsPath('/operations')).toBe('/settings/support/diagnostics'); + expect(buildLegacyOperationsSettingsPath('/operations/reporting')).toBe( + '/settings/support/reporting', + ); + expect(buildLegacyOperationsSettingsPath('/operations/logs')).toBe('/settings/support/logs'); + }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/useSettingsShellState.test.ts b/frontend-modern/src/components/Settings/__tests__/useSettingsShellState.test.ts index 30206cc72..27d144d6a 100644 --- a/frontend-modern/src/components/Settings/__tests__/useSettingsShellState.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/useSettingsShellState.test.ts @@ -22,7 +22,7 @@ describe('useSettingsShellState', () => { activeTab: () => 'infrastructure-operations', }); - expect(state.headerMeta().title).toBe('Infrastructure Operations'); + expect(state.headerMeta().title).toBe('Connections & Inventory'); expect(state.headerMeta().description).toBe( 'Review the current monitored-system inventory, reporting posture, and connected platform coverage. Setup changes stay unavailable in this read-only session.', ); diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index fc6b81969..c0822a103 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -13,7 +13,7 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { 'Add and manage Proxmox VE, Backup Server, and Mail Gateway connections when the unified agent is not available on the host.', }, 'infrastructure-operations': { - title: 'Infrastructure Operations', + title: 'Connections & Inventory', description: `Bring infrastructure into Pulse, manage API-backed platform connections, and control which systems are actively reporting. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`, }, @@ -45,6 +45,18 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { title: SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle, description: SELF_HOSTED_PRO_BILLING_PRESENTATION.shellDescription, }, + 'support-diagnostics': { + title: 'Diagnostics & Health', + description: 'Run health checks, validate connectivity, and export troubleshooting snapshots.', + }, + 'support-reporting': { + title: 'Data & Reports', + description: 'Export inventory data and generate performance reports from the canonical settings shell.', + }, + 'support-logs': { + title: 'System Logs', + description: 'Inspect the live Pulse log stream and download the captured buffer for support work.', + }, 'organization-overview': { title: 'Organization Overview', description: 'Review organization metadata, membership footprint, and ownership.', diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index 9668bde07..ad482f76c 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -16,6 +16,8 @@ import BadgeCheck from 'lucide-solid/icons/badge-check'; import Building2 from 'lucide-solid/icons/building-2'; import Share2 from 'lucide-solid/icons/share-2'; import CreditCard from 'lucide-solid/icons/credit-card'; +import FileText from 'lucide-solid/icons/file-text'; +import Terminal from 'lucide-solid/icons/terminal'; import { PulseLogoIcon } from '@/components/icons/PulseLogoIcon'; import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation'; import type { @@ -31,7 +33,7 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ items: [ { id: 'infrastructure-operations', - label: 'Operations', + label: 'Connections & Inventory', icon: Bot, iconProps: { strokeWidth: 2 }, }, @@ -138,6 +140,33 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ }, ], }, + { + id: 'support', + label: 'Support', + items: [ + { + id: 'support-diagnostics', + label: 'Diagnostics & Health', + icon: Activity, + iconProps: { strokeWidth: 2 }, + hideWhenDemoMode: true, + }, + { + id: 'support-reporting', + label: 'Data & Reports', + icon: FileText, + iconProps: { strokeWidth: 2 }, + hideWhenDemoMode: true, + }, + { + id: 'support-logs', + label: 'System Logs', + icon: Terminal, + iconProps: { strokeWidth: 2 }, + hideWhenDemoMode: true, + }, + ], + }, { id: 'security', label: 'Security', diff --git a/frontend-modern/src/components/Settings/settingsNavVisibility.ts b/frontend-modern/src/components/Settings/settingsNavVisibility.ts index 696ce0be2..2b438d635 100644 --- a/frontend-modern/src/components/Settings/settingsNavVisibility.ts +++ b/frontend-modern/src/components/Settings/settingsNavVisibility.ts @@ -7,6 +7,7 @@ export interface SettingsNavVisibilityContext { hasFeature: (feature: string) => boolean; runtimeCapabilitiesLoaded: () => boolean; presentationPolicyHidesCommercial?: boolean; + presentationPolicyIsDemoMode?: boolean; presentationPolicyHidesOrganizations?: boolean; presentationPolicyResolved?: boolean; hostedModeEnabled?: boolean; @@ -53,6 +54,16 @@ export function shouldHideSettingsNavItem( } } + if (item.hideWhenDemoMode) { + if (context.presentationPolicyResolved === false) { + return true; + } + + if (context.presentationPolicyIsDemoMode) { + return true; + } + } + if ( item.requiredCapability && context.settingsCapabilitiesResolved && diff --git a/frontend-modern/src/components/Settings/settingsNavigationModel.ts b/frontend-modern/src/components/Settings/settingsNavigationModel.ts index a06aef23b..8b8b175ce 100644 --- a/frontend-modern/src/components/Settings/settingsNavigationModel.ts +++ b/frontend-modern/src/components/Settings/settingsNavigationModel.ts @@ -17,6 +17,9 @@ export type SettingsTab = | 'system-ai' | 'system-relay' | 'system-billing' + | 'support-diagnostics' + | 'support-reporting' + | 'support-logs' | 'organization-overview' | 'organization-access' | 'organization-billing' @@ -37,7 +40,12 @@ export type ProxmoxPlatformType = Extract< 'proxmox-pve' | 'proxmox-pbs' | 'proxmox-pmg' >; -export type SettingsNavGroupId = 'infrastructure' | 'organization' | 'system' | 'security'; +export type SettingsNavGroupId = + | 'infrastructure' + | 'organization' + | 'system' + | 'support' + | 'security'; export interface SettingsNavItem { id: SettingsTab; @@ -51,6 +59,7 @@ export interface SettingsNavItem { hostedOnly?: boolean; hideWhenCommercialHidden?: boolean; hideWhenOrganizationHidden?: boolean; + hideWhenDemoMode?: boolean; requiredCapability?: keyof SecurityStatusSettingsCapabilities; badge?: string; features?: string[]; @@ -78,11 +87,16 @@ 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 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'; const SYSTEM_BILLING_PREFIX = SELF_HOSTED_PRO_BILLING_ROUTE; const LEGACY_SYSTEM_PRO_PREFIX = '/settings/system-pro'; @@ -143,6 +157,15 @@ export function resolveCanonicalSettingsPath(path: string): string | null { if (normalizedPath === LEGACY_DOCKER_PREFIX) { return settingsTabPath(DEFAULT_SETTINGS_TAB); } + if (normalizedPath === SUPPORT_PREFIX) { + return SUPPORT_DIAGNOSTICS_PREFIX; + } + if (normalizedPath === LEGACY_SETTINGS_OPERATIONS_PREFIX) { + return SUPPORT_DIAGNOSTICS_PREFIX; + } + if (normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/`)) { + return buildLegacyOperationsSettingsPath(normalizedPath); + } if (normalizedPath === LEGACY_PROXMOX_API_PREFIX) { return PROXMOX_PREFIX; } @@ -218,6 +241,10 @@ export function deriveTabFromPath(path: string): SettingsTab { if (canonicalPath.includes('/settings/system-relay')) return 'system-relay'; 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'; + if (canonicalPath.startsWith(SUPPORT_DIAGNOSTICS_PREFIX) || canonicalPath === SUPPORT_PREFIX) + return 'support-diagnostics'; if (canonicalPath.includes('/settings/organization/access')) return 'organization-access'; if (canonicalPath.includes('/settings/organization/sharing')) return 'organization-sharing'; if (canonicalPath.includes('/settings/organization/billing-admin')) @@ -317,6 +344,15 @@ export function deriveTabFromQuery(search: string): SettingsTab | null { case 'system-pro': case 'system-billing': return 'system-billing'; + case 'diagnostics': + case 'support-diagnostics': + return 'support-diagnostics'; + case 'reporting': + case 'support-reporting': + return 'support-reporting'; + case 'logs': + case 'support-logs': + return 'support-logs'; case 'api': return 'api'; case 'docker': @@ -374,7 +410,37 @@ export function settingsTabPath(tab: SettingsTab): string { return '/settings/system-relay'; case 'system-billing': return SELF_HOSTED_PRO_BILLING_PLAN_ROUTE; + case 'support-diagnostics': + return SUPPORT_DIAGNOSTICS_PREFIX; + case 'support-reporting': + return SUPPORT_REPORTING_PREFIX; + case 'support-logs': + return SUPPORT_LOGS_PREFIX; default: return `/settings/${tab}`; } } + +export function buildLegacyOperationsSettingsPath(path: string): string { + const normalizedPath = normalizeSettingsPath(path); + + if ( + normalizedPath === '/operations/logs' || + normalizedPath.startsWith('/operations/logs/') || + normalizedPath === `${LEGACY_SETTINGS_OPERATIONS_PREFIX}/logs` || + normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/logs/`) + ) { + return SUPPORT_LOGS_PREFIX; + } + + if ( + normalizedPath === '/operations/reporting' || + normalizedPath.startsWith('/operations/reporting/') || + normalizedPath === `${LEGACY_SETTINGS_OPERATIONS_PREFIX}/reporting` || + normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/reporting/`) + ) { + return SUPPORT_REPORTING_PREFIX; + } + + return SUPPORT_DIAGNOSTICS_PREFIX; +} diff --git a/frontend-modern/src/components/Settings/settingsPanelRegistry.ts b/frontend-modern/src/components/Settings/settingsPanelRegistry.ts index f848efdf4..d8abb7b80 100644 --- a/frontend-modern/src/components/Settings/settingsPanelRegistry.ts +++ b/frontend-modern/src/components/Settings/settingsPanelRegistry.ts @@ -76,6 +76,15 @@ export const createSettingsPanelRegistry = ( 'system-billing': { component: context.systemBillingPanel, }, + 'support-diagnostics': { + component: SETTINGS_PANEL_REGISTRY_LOADERS.DiagnosticsPanel, + }, + 'support-reporting': { + component: SETTINGS_PANEL_REGISTRY_LOADERS.ReportingPanel, + }, + 'support-logs': { + component: SETTINGS_PANEL_REGISTRY_LOADERS.SystemLogsPanel, + }, 'organization-overview': { component: SETTINGS_PANEL_REGISTRY_LOADERS.OrganizationOverviewPanel, getProps: context.getOrganizationOverviewPanelProps, diff --git a/frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts b/frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts index 0cabff712..ca537dbed 100644 --- a/frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts +++ b/frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts @@ -4,6 +4,9 @@ export const SETTINGS_PANEL_REGISTRY_LOADERS = { APIAccessPanel: lazy(() => import('./APIAccessPanel').then((m) => ({ default: m.APIAccessPanel })), ), + DiagnosticsPanel: lazy(() => + import('./DiagnosticsPanel').then((m) => ({ default: m.DiagnosticsPanel })), + ), AuditLogPanel: lazy(() => import('./AuditLogPanel')), AuditWebhookPanel: lazy(() => import('./AuditWebhookPanel').then((m) => ({ default: m.AuditWebhookPanel })), @@ -22,6 +25,9 @@ export const SETTINGS_PANEL_REGISTRY_LOADERS = { RecoverySettingsPanel: lazy(() => import('./RecoverySettingsPanel').then((m) => ({ default: m.RecoverySettingsPanel })), ), + ReportingPanel: lazy(() => + import('./ReportingPanel').then((m) => ({ default: m.ReportingPanel })), + ), RelaySettingsPanel: lazy(() => import('./RelaySettingsPanel').then((m) => ({ default: m.RelaySettingsPanel })), ), @@ -32,6 +38,9 @@ export const SETTINGS_PANEL_REGISTRY_LOADERS = { SecurityOverviewPanel: lazy(() => import('./SecurityOverviewPanel').then((m) => ({ default: m.SecurityOverviewPanel })), ), + SystemLogsPanel: lazy(() => + import('./SystemLogsPanel').then((m) => ({ default: m.SystemLogsPanel })), + ), UpdatesSettingsPanel: lazy(() => import('./UpdatesSettingsPanel').then((m) => ({ default: m.UpdatesSettingsPanel })), ), diff --git a/frontend-modern/src/components/Settings/useSettingsAccess.ts b/frontend-modern/src/components/Settings/useSettingsAccess.ts index 6fab3e9c2..488430afb 100644 --- a/frontend-modern/src/components/Settings/useSettingsAccess.ts +++ b/frontend-modern/src/components/Settings/useSettingsAccess.ts @@ -2,6 +2,7 @@ import { Accessor, createEffect, createMemo, createSignal } from 'solid-js'; import { presentationPolicyHidesCommercialSurfaces, presentationPolicyHidesOrganizationSurfaces, + presentationPolicyIsDemoMode, sessionPresentationPolicyResolved, syncSessionPresentationPolicy, } from '@/stores/sessionPresentationPolicy'; @@ -40,6 +41,16 @@ export function useSettingsAccess({ const presentationPolicyResolved = createMemo( () => securityStatus() !== null || sessionPresentationPolicyResolved(), ); + const demoMode = createMemo(() => { + const resolvedSecurityStatus = securityStatus(); + if (resolvedSecurityStatus) { + return ( + resolvedSecurityStatus.presentationPolicy?.demoMode === true || + resolvedSecurityStatus.sessionCapabilities?.demoMode === true + ); + } + return presentationPolicyIsDemoMode(); + }); const organizationSurfacesHidden = createMemo(() => { const resolvedSecurityStatus = securityStatus(); if (resolvedSecurityStatus) { @@ -65,6 +76,7 @@ export function useSettingsAccess({ hasFeature, runtimeCapabilitiesLoaded, presentationPolicyHidesCommercial: commercialSurfacesHidden(), + presentationPolicyIsDemoMode: demoMode(), presentationPolicyHidesOrganizations: organizationSurfacesHidden(), presentationPolicyResolved: presentationPolicyResolved(), hostedModeEnabled, diff --git a/frontend-modern/src/components/Settings/useSettingsShellState.ts b/frontend-modern/src/components/Settings/useSettingsShellState.ts index fc434f87c..a6d271095 100644 --- a/frontend-modern/src/components/Settings/useSettingsShellState.ts +++ b/frontend-modern/src/components/Settings/useSettingsShellState.ts @@ -13,7 +13,7 @@ export function useSettingsShellState({ activeTab }: UseSettingsShellStateParams const tab = activeTab(); if (tab === 'infrastructure-operations' && presentationPolicyIsReadOnly()) { return { - title: 'Infrastructure Operations', + title: 'Connections & Inventory', description: 'Review the current monitored-system inventory, reporting posture, and connected platform coverage. Setup changes stay unavailable in this read-only session.', }; diff --git a/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx b/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx index 8cb52d594..76c1054e9 100644 --- a/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx +++ b/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx @@ -518,8 +518,8 @@ Keep these credentials secure!

{completionViewModel().hasConnectedSystems - ? 'Pulse already has a live monitored system. Open the dashboard to confirm the first overview, then return to Infrastructure Operations when you want to continue with the next system path.' - : 'The canonical install flow now lives in Infrastructure Operations. Open that workspace to continue with the first-host install token Pulse prepares from setup, adjust the agent connection URL only if needed, configure TLS or custom CA options, and copy the correct command for the first system you want Pulse to monitor.'} + ? 'Pulse already has a live monitored system. Open the dashboard to confirm the first overview, then return to Connections & Inventory when you want to continue with the next system path.' + : 'The canonical install flow now lives in Connections & Inventory. Open that workspace to continue with the first-host install token Pulse prepares from setup, adjust the agent connection URL only if needed, configure TLS or custom CA options, and copy the correct command for the first system you want Pulse to monitor.'}

diff --git a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts index 7260f6bd3..b768d1245 100644 --- a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts +++ b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts @@ -104,7 +104,7 @@ describe('SetupCompletionPanel guardrails', () => { expect(setupCompletionPanelSource).not.toContain('hasConnectedAgents'); expect(setupCompletionPanelSource).not.toContain('connectedAgents().length'); expect(setupCompletionPanelSource).not.toContain( - 'You can return here later from Infrastructure Operations if you skip install for now.', + 'You can return here later from Connections & Inventory if you skip install for now.', ); }); }); diff --git a/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx b/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx index 3f40403b6..036e81589 100644 --- a/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx @@ -99,6 +99,8 @@ describe('MobileNavBar', () => { const buttons = container.querySelectorAll('button[data-tab-id]'); expect(buttons[0]).toHaveAttribute('data-tab-id', 'dashboard'); expect(buttons[1]).toHaveAttribute('data-tab-id', 'storage'); + expect(buttons[2]).toHaveAttribute('data-tab-id', 'alerts'); + expect(buttons[3]).toHaveAttribute('data-tab-id', 'settings'); expect(screen.getByText('2')).toBeInTheDocument(); expect(screen.getByText('3')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/shared/mobileNavBarModel.ts b/frontend-modern/src/components/shared/mobileNavBarModel.ts index 3d2818b74..3e45b7c76 100644 --- a/frontend-modern/src/components/shared/mobileNavBarModel.ts +++ b/frontend-modern/src/components/shared/mobileNavBarModel.ts @@ -14,7 +14,7 @@ export type MobileNavBarPlatformTab = { }; export type MobileNavBarUtilityTab = { - id: 'alerts' | 'ai' | 'operations' | 'settings'; + id: 'alerts' | 'ai' | 'settings'; label: string; route: string; tooltip: string; @@ -40,7 +40,7 @@ const MOBILE_NAV_PLATFORM_PRIORITY = [ 'recovery', ] as const; -const MOBILE_NAV_UTILITY_PRIORITY = ['alerts', 'ai', 'operations', 'settings'] as const; +const MOBILE_NAV_UTILITY_PRIORITY = ['alerts', 'ai', 'settings'] as const; export function buildOrderedMobileNavTabs( tabs: T[], diff --git a/frontend-modern/src/features/operations/OperationsPageSurface.tsx b/frontend-modern/src/features/operations/OperationsPageSurface.tsx deleted file mode 100644 index 4a0114b7c..000000000 --- a/frontend-modern/src/features/operations/OperationsPageSurface.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Show, Suspense, createEffect, createMemo, type Component, type JSX } from 'solid-js'; -import { useLocation, useNavigate } from '@solidjs/router'; -import ActivityIcon from 'lucide-solid/icons/activity'; -import FileTextIcon from 'lucide-solid/icons/file-text'; -import TerminalIcon from 'lucide-solid/icons/terminal'; -import { DiagnosticsPanel } from '@/components/Settings/DiagnosticsPanel'; -import { ReportingPanel } from '@/components/Settings/ReportingPanel'; -import { SystemLogsPanel } from '@/components/Settings/SystemLogsPanel'; -import { PageHeader } from '@/components/shared/PageHeader'; -import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs'; -import { DASHBOARD_PATH } from '@/routing/resourceLinks'; -import { presentationPolicyIsDemoMode } from '@/stores/sessionPresentationPolicy'; -import { - buildOperationsPath, - getOperationsTabFromPath, - operationsSurfaceHiddenInDemoMode, - OPERATIONS_TABS, - type OperationsTabId, -} from '@/features/operations/operationsPageModel'; - -const operationsTabIcons: Record> = { - diagnostics: ActivityIcon, - reporting: FileTextIcon, - logs: TerminalIcon, -}; - -export function OperationsPageSurface() { - const location = useLocation(); - const navigate = useNavigate(); - - const hiddenInDemoMode = createMemo(() => - operationsSurfaceHiddenInDemoMode(presentationPolicyIsDemoMode()), - ); - const activeTab = createMemo(() => getOperationsTabFromPath(location.pathname)); - - createEffect(() => { - if (hiddenInDemoMode()) { - navigate(DASHBOARD_PATH, { replace: true }); - } - }); - - const tabs = createMemo(() => - hiddenInDemoMode() - ? [] - : OPERATIONS_TABS.map((tab) => { - const Icon = operationsTabIcons[tab.id]; - return { - value: tab.id, - label: ( - - - {tab.label} - - ) satisfies JSX.Element, - }; - }), - ); - - const handleTabChange = (tabId: string) => { - navigate(buildOperationsPath(tabId as OperationsTabId)); - }; - - return ( - -
- - -
- -
- -
- -
-
- } - > - {activeTab() === 'diagnostics' && } - {activeTab() === 'reporting' && } - {activeTab() === 'logs' && } - -
-
- - ); -} - -export default OperationsPageSurface; diff --git a/frontend-modern/src/features/operations/__tests__/OperationsPageSurface.demoMode.test.tsx b/frontend-modern/src/features/operations/__tests__/OperationsPageSurface.demoMode.test.tsx deleted file mode 100644 index 878b8c8c8..000000000 --- a/frontend-modern/src/features/operations/__tests__/OperationsPageSurface.demoMode.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { OperationsPageSurface } from '@/features/operations/OperationsPageSurface'; - -const navigateSpy = vi.hoisted(() => vi.fn()); -const presentationPolicyIsDemoModeMock = vi.hoisted(() => vi.fn(() => false)); -const locationState = vi.hoisted(() => ({ - pathname: '/operations', - hash: '', - search: '', - query: {}, -})); - -vi.mock('@solidjs/router', async () => { - const actual = await vi.importActual('@solidjs/router'); - return { - ...actual, - useLocation: () => locationState, - useNavigate: () => navigateSpy, - }; -}); - -vi.mock('@/stores/sessionPresentationPolicy', () => ({ - presentationPolicyIsDemoMode: () => presentationPolicyIsDemoModeMock(), -})); - -vi.mock('@/components/Settings/DiagnosticsPanel', () => ({ - DiagnosticsPanel: () =>
Diagnostics
, -})); - -vi.mock('@/components/Settings/ReportingPanel', () => ({ - ReportingPanel: () =>
Reporting
, -})); - -vi.mock('@/components/Settings/SystemLogsPanel', () => ({ - SystemLogsPanel: () =>
Logs
, -})); - -describe('OperationsPageSurface demo mode', () => { - beforeEach(() => { - cleanup(); - navigateSpy.mockReset(); - presentationPolicyIsDemoModeMock.mockReset(); - presentationPolicyIsDemoModeMock.mockReturnValue(false); - locationState.pathname = '/operations'; - locationState.hash = ''; - locationState.search = ''; - }); - - afterEach(() => cleanup()); - - it('redirects demo sessions back to the dashboard and hides operations chrome', async () => { - presentationPolicyIsDemoModeMock.mockReturnValue(true); - - render(() => ); - - await waitFor(() => { - expect(navigateSpy).toHaveBeenCalledWith('/dashboard', { replace: true }); - }); - expect(screen.queryByText('Diagnostics & Health')).not.toBeInTheDocument(); - expect(screen.queryByTestId('diagnostics-panel')).not.toBeInTheDocument(); - }); - - it('keeps operations tabs available outside demo mode', async () => { - render(() => ); - - await waitFor(() => { - expect(screen.getByText('Diagnostics & Health')).toBeInTheDocument(); - }); - expect(screen.getByTestId('diagnostics-panel')).toBeInTheDocument(); - expect(navigateSpy).not.toHaveBeenCalledWith('/dashboard', { replace: true }); - }); -}); diff --git a/frontend-modern/src/features/operations/operationsPageModel.ts b/frontend-modern/src/features/operations/operationsPageModel.ts deleted file mode 100644 index 2069deaa0..000000000 --- a/frontend-modern/src/features/operations/operationsPageModel.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type OperationsTabId = 'diagnostics' | 'reporting' | 'logs'; - -export interface OperationsTabDefinition { - id: OperationsTabId; - label: string; - description: string; -} - -export const OPERATIONS_TABS: readonly OperationsTabDefinition[] = [ - { - id: 'diagnostics', - label: 'Diagnostics & Health', - description: 'System health, connection tests, and troubleshooting', - }, - { - id: 'reporting', - label: 'Data Export & Reports', - description: 'Export system metrics and configuration data', - }, - { - id: 'logs', - label: 'System Logs', - description: 'View real-time Pulse system logs', - }, -]; - -export function operationsSurfaceHiddenInDemoMode(demoMode: boolean): boolean { - return demoMode; -} - -export function getOperationsTabFromPath(pathname: string): OperationsTabId { - const lastPathSegment = pathname.split('/').pop() || ''; - if (lastPathSegment === 'reporting') return 'reporting'; - if (lastPathSegment === 'logs') return 'logs'; - return 'diagnostics'; -} - -export function buildOperationsPath(tabId: OperationsTabId): string { - return `/operations/${tabId}`; -} diff --git a/frontend-modern/src/pages/Operations.tsx b/frontend-modern/src/pages/Operations.tsx index ac3f07adf..2ec9b3861 100644 --- a/frontend-modern/src/pages/Operations.tsx +++ b/frontend-modern/src/pages/Operations.tsx @@ -1,8 +1,10 @@ -import type { Component } from 'solid-js'; -import { OperationsPageSurface } from '@/features/operations/OperationsPageSurface'; +import { Navigate, useLocation } from '@solidjs/router'; +import { buildLegacyOperationsSettingsPath } from '@/components/Settings/settingsNavigationModel'; -export const Operations: Component = () => { - return ; +export const Operations = () => { + const location = useLocation(); + const canonicalPath = buildLegacyOperationsSettingsPath(location.pathname); + return ; }; export default Operations; diff --git a/frontend-modern/src/pages/__tests__/Operations.helpers.test.ts b/frontend-modern/src/pages/__tests__/Operations.helpers.test.ts index c26d2ab87..4f7288e01 100644 --- a/frontend-modern/src/pages/__tests__/Operations.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Operations.helpers.test.ts @@ -1,30 +1,26 @@ import { describe, expect, it } from 'vitest'; import appSource from '@/App.tsx?raw'; import operationsPageRouteSource from '@/pages/Operations.tsx?raw'; -import operationsPageSurfaceSource from '@/features/operations/OperationsPageSurface.tsx?raw'; -import operationsPageModelSource from '@/features/operations/operationsPageModel.ts?raw'; +import settingsNavigationModelSource from '@/components/Settings/settingsNavigationModel.ts?raw'; -describe('operations page route shell', () => { - it('keeps App routing on a page shell instead of a page-local route controller', () => { +describe('legacy operations route plumbing', () => { + it('keeps /operations as a redirect-only compatibility page', () => { expect(appSource).toContain("const OperationsPage = lazy(() => import('./pages/Operations'));"); expect(operationsPageRouteSource).toContain( - "import { OperationsPageSurface } from '@/features/operations/OperationsPageSurface';", + "import { Navigate, useLocation } from '@solidjs/router';", + ); + expect(operationsPageRouteSource).toContain( + "import { buildLegacyOperationsSettingsPath } from '@/components/Settings/settingsNavigationModel';", + ); + expect(operationsPageRouteSource).toContain( + 'const canonicalPath = buildLegacyOperationsSettingsPath(location.pathname);', + ); + expect(operationsPageRouteSource).toContain( + "return ;", + ); + expect(operationsPageRouteSource).not.toContain('OperationsPageSurface'); + expect(settingsNavigationModelSource).toContain( + 'export function buildLegacyOperationsSettingsPath', ); - expect(operationsPageRouteSource).toContain(''); - expect(operationsPageRouteSource).not.toContain('useLocation'); - expect(operationsPageRouteSource).not.toContain('useNavigate'); - expect(operationsPageRouteSource).not.toContain('createSignal'); - expect(operationsPageSurfaceSource).toContain('@/components/shared/Subtabs'); - expect(operationsPageSurfaceSource).toContain("import { PageHeader } from '@/components/shared/PageHeader';"); - expect(operationsPageSurfaceSource).toContain(' { expect(getActiveTabForPath('/alerts/open')).toBe('alerts'); expect(getActiveTabForPath('/patrol')).toBe('ai'); expect(getActiveTabForPath('/ai')).toBe('ai'); - expect(getActiveTabForPath('/operations')).toBe('operations'); - expect(getActiveTabForPath('/operations/diagnostics')).toBe('operations'); - expect(getActiveTabForPath('/operations/logs')).toBe('operations'); + expect(getActiveTabForPath('/operations')).toBe('settings'); + expect(getActiveTabForPath('/operations/diagnostics')).toBe('settings'); + expect(getActiveTabForPath('/operations/logs')).toBe('settings'); expect(getActiveTabForPath('/settings/security')).toBe('settings'); }); }); diff --git a/frontend-modern/src/routing/navigation.ts b/frontend-modern/src/routing/navigation.ts index 6a9274d9d..a1c75cec3 100644 --- a/frontend-modern/src/routing/navigation.ts +++ b/frontend-modern/src/routing/navigation.ts @@ -8,8 +8,7 @@ export type AppTabId = | 'recovery' | 'alerts' | 'ai' - | 'settings' - | 'operations'; + | 'settings'; export function getActiveTabForPath(path: string): AppTabId { if (path.startsWith('/dashboard')) return 'dashboard'; @@ -21,6 +20,6 @@ export function getActiveTabForPath(path: string): AppTabId { if (path.startsWith('/alerts')) return 'alerts'; if (path.startsWith(PATROL_PATH) || path.startsWith('/ai')) return 'ai'; if (path.startsWith('/settings')) return 'settings'; - if (path.startsWith('/operations')) return 'operations'; + if (path.startsWith('/operations')) return 'settings'; return 'infrastructure'; } diff --git a/frontend-modern/src/routing/routePreload.ts b/frontend-modern/src/routing/routePreload.ts index 3be62d11e..61f2bf882 100644 --- a/frontend-modern/src/routing/routePreload.ts +++ b/frontend-modern/src/routing/routePreload.ts @@ -18,7 +18,6 @@ const ROOT_WORKLOADS_PATH = buildWorkloadsPath(); const STORAGE_PATH = buildStoragePath(); const RECOVERY_ROUTE_PATH = buildRecoveryPath(); const ALERTS_PATH = '/alerts'; -const OPERATIONS_PATH = '/operations'; const SETTINGS_PATH = '/settings'; const routePreloadCache = new Map>(); @@ -74,12 +73,6 @@ const ROUTE_PRELOADERS: readonly RoutePreloader[] = [ preload: () => import('@/pages/AIIntelligence').then(() => undefined), }, - { - id: 'operations', - matches: (route) => route === OPERATIONS_PATH || route.startsWith(`${OPERATIONS_PATH}/`), - preload: () => - import('@/pages/Operations').then(() => undefined), - }, { id: 'settings', matches: (route) => route === SETTINGS_PATH || route.startsWith(`${SETTINGS_PATH}/`), diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 0b2442b62..f8d2f8479 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -256,8 +256,6 @@ import useChatSource from '@/components/AI/Chat/hooks/useChat.ts?raw'; import patrolStatusBarSource from '@/components/patrol/PatrolStatusBar.tsx?raw'; import patrolFormatSource from '@/utils/patrolFormat.ts?raw'; import aiFindingPresentationSource from '@/utils/aiFindingPresentation.ts?raw'; -import operationsPageSurfaceSource from '@/features/operations/OperationsPageSurface.tsx?raw'; -import operationsPageModelSource from '@/features/operations/operationsPageModel.ts?raw'; import chatIdentifiersSource from '@/utils/chatIdentifiers.ts?raw'; import resourceIdentitySource from '@/utils/resourceIdentity.ts?raw'; import stringUtilsSource from '@/utils/stringUtils.ts?raw'; @@ -4388,21 +4386,14 @@ describe('frontend resource type boundaries', () => { '!presentationPolicyHidesUpgradePrompts() && state.alertAnalysisLocked()', ); expect(operationsPageRouteSource).toContain( - "import { OperationsPageSurface } from '@/features/operations/OperationsPageSurface';", + "import { Navigate, useLocation } from '@solidjs/router';", + ); + expect(operationsPageRouteSource).toContain('buildLegacyOperationsSettingsPath'); + expect(operationsPageRouteSource).toContain(''); + expect(operationsPageRouteSource).not.toContain('OperationsPageSurface'); + expect(settingsNavigationModelSource).toContain( + 'export function buildLegacyOperationsSettingsPath', ); - expect(operationsPageRouteSource).toContain(''); - expect(operationsPageRouteSource).not.toContain('useLocation'); - expect(operationsPageRouteSource).not.toContain('useNavigate'); - expect(operationsPageSurfaceSource).toContain('@/components/shared/Subtabs'); - expect(operationsPageSurfaceSource).toContain('getOperationsTabFromPath'); - expect(operationsPageSurfaceSource).toContain('buildOperationsPath'); - expect(operationsPageSurfaceSource).toContain(''); - expect(operationsPageSurfaceSource).toContain(''); - expect(operationsPageSurfaceSource).toContain(''); - expect(operationsPageSurfaceSource).not.toContain('-webkit-overflow-scrolling'); - expect(operationsPageModelSource).toContain('export const OPERATIONS_TABS'); - expect(operationsPageModelSource).toContain('export function getOperationsTabFromPath'); - expect(operationsPageModelSource).toContain('export function buildOperationsPath'); expect(reportingPanelSource).toContain('OperationsPanel'); expect(systemLogsPanelSource).toContain('OperationsPanel'); expect(systemLogsPanelSource).toContain('./useSystemLogsPanelState');