mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Move operations tools into settings support
This commit is contained in:
parent
3a5f36da19
commit
4e4fcb5dbe
37 changed files with 364 additions and 403 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 `<h1>` 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <Navigate href={`${canonicalPath}${location.search ?? ''}`} />;
|
||||
};
|
||||
const LegacyPatrolRouteRedirect = () => {
|
||||
const location = useLocation();
|
||||
const canonicalPath =
|
||||
|
|
@ -497,7 +490,6 @@ function App() {
|
|||
<Route path="/alerts/*" component={AlertsPage} />
|
||||
<Route path={`${ROOT_PATROL_PATH}/*`} component={AIIntelligencePage} />
|
||||
<Route path="/ai/*" component={LegacyPatrolRouteRedirect} />
|
||||
<Route path="/settings/operations/*" component={LegacyOperationsSettingsRedirect} />
|
||||
<Route path="/settings/*" component={SettingsRoute} />
|
||||
<Route path="/operations/*" component={OperationsPage} />
|
||||
<Route path="*all" component={NotFoundPage} />
|
||||
|
|
|
|||
|
|
@ -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: <ActivityIcon class="w-4 h-4 shrink-0" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasSettingsAccess) {
|
||||
tabs.push({
|
||||
id: 'settings',
|
||||
|
|
|
|||
|
|
@ -84,8 +84,11 @@ describe('App architecture', () => {
|
|||
expect(appLayoutSource).toContain(
|
||||
'<ReleaseCandidateBanner version={props.versionInfo()?.version} />',
|
||||
);
|
||||
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(');
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -518,8 +518,8 @@ Keep these credentials secure!
|
|||
</h3>
|
||||
<p class="mt-2 text-xs text-muted max-w-xl">
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-sm bg-blue-50 px-2 py-1 text-[10px] font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<T extends { id: string }>(
|
||||
tabs: T[],
|
||||
|
|
|
|||
|
|
@ -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<OperationsTabId, Component<{ class?: string }>> = {
|
||||
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<SubtabOption[]>(() =>
|
||||
hiddenInDemoMode()
|
||||
? []
|
||||
: OPERATIONS_TABS.map((tab) => {
|
||||
const Icon = operationsTabIcons[tab.id];
|
||||
return {
|
||||
value: tab.id,
|
||||
label: (
|
||||
<span class="inline-flex items-center gap-2.5" title={tab.description}>
|
||||
<Icon class="h-4 w-4" />
|
||||
<span>{tab.label}</span>
|
||||
</span>
|
||||
) satisfies JSX.Element,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
navigate(buildOperationsPath(tabId as OperationsTabId));
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={!hiddenInDemoMode()}>
|
||||
<div class="space-y-6">
|
||||
<PageHeader
|
||||
title="Operations"
|
||||
description="Run diagnostics, review generated reports, and inspect system logs without leaving the app."
|
||||
/>
|
||||
|
||||
<div class="mb-6">
|
||||
<Subtabs
|
||||
value={activeTab()}
|
||||
onChange={handleTabChange}
|
||||
tabs={tabs()}
|
||||
ariaLabel="Operations"
|
||||
class="rounded-md border border-border bg-surface-alt p-1.5 sm:w-max"
|
||||
listClass="gap-2 overflow-x-auto scrollbar-hide"
|
||||
tabClass="min-h-10 whitespace-nowrap rounded-md border border-transparent px-4 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 animate-fade-in animate-duration-200">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex justify-center p-6">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activeTab() === 'diagnostics' && <DiagnosticsPanel />}
|
||||
{activeTab() === 'reporting' && <ReportingPanel />}
|
||||
{activeTab() === 'logs' && <SystemLogsPanel />}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export default OperationsPageSurface;
|
||||
|
|
@ -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<typeof import('@solidjs/router')>('@solidjs/router');
|
||||
return {
|
||||
...actual,
|
||||
useLocation: () => locationState,
|
||||
useNavigate: () => navigateSpy,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/stores/sessionPresentationPolicy', () => ({
|
||||
presentationPolicyIsDemoMode: () => presentationPolicyIsDemoModeMock(),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/DiagnosticsPanel', () => ({
|
||||
DiagnosticsPanel: () => <div data-testid="diagnostics-panel">Diagnostics</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/ReportingPanel', () => ({
|
||||
ReportingPanel: () => <div data-testid="reporting-panel">Reporting</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/SystemLogsPanel', () => ({
|
||||
SystemLogsPanel: () => <div data-testid="system-logs-panel">Logs</div>,
|
||||
}));
|
||||
|
||||
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(() => <OperationsPageSurface />);
|
||||
|
||||
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(() => <OperationsPageSurface />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Diagnostics & Health')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('diagnostics-panel')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalledWith('/dashboard', { replace: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
@ -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 <OperationsPageSurface />;
|
||||
export const Operations = () => {
|
||||
const location = useLocation();
|
||||
const canonicalPath = buildLegacyOperationsSettingsPath(location.pathname);
|
||||
return <Navigate href={`${canonicalPath}${location.search ?? ''}`} />;
|
||||
};
|
||||
|
||||
export default Operations;
|
||||
|
|
|
|||
|
|
@ -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 <Navigate href={`${canonicalPath}${location.search ?? ''}`} />;",
|
||||
);
|
||||
expect(operationsPageRouteSource).not.toContain('OperationsPageSurface');
|
||||
expect(settingsNavigationModelSource).toContain(
|
||||
'export function buildLegacyOperationsSettingsPath',
|
||||
);
|
||||
expect(operationsPageRouteSource).toContain('<OperationsPageSurface />');
|
||||
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('<PageHeader');
|
||||
expect(operationsPageSurfaceSource).toContain('title="Operations"');
|
||||
expect(operationsPageSurfaceSource).toContain('getOperationsTabFromPath');
|
||||
expect(operationsPageSurfaceSource).toContain('buildOperationsPath');
|
||||
expect(operationsPageSurfaceSource).toContain('operationsSurfaceHiddenInDemoMode');
|
||||
expect(operationsPageSurfaceSource).not.toContain('-webkit-overflow-scrolling');
|
||||
expect(operationsPageModelSource).toContain('export const OPERATIONS_TABS');
|
||||
expect(operationsPageModelSource).toContain('export function operationsSurfaceHiddenInDemoMode');
|
||||
expect(operationsPageModelSource).toContain('export function getOperationsTabFromPath');
|
||||
expect(operationsPageModelSource).toContain('export function buildOperationsPath');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ describe('navigation routing helpers', () => {
|
|||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, Promise<void>>();
|
||||
|
||||
|
|
@ -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}/`),
|
||||
|
|
|
|||
|
|
@ -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('<Navigate href={`${canonicalPath}${location.search ?? \'\'}`} />');
|
||||
expect(operationsPageRouteSource).not.toContain('OperationsPageSurface');
|
||||
expect(settingsNavigationModelSource).toContain(
|
||||
'export function buildLegacyOperationsSettingsPath',
|
||||
);
|
||||
expect(operationsPageRouteSource).toContain('<OperationsPageSurface />');
|
||||
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('<DiagnosticsPanel />');
|
||||
expect(operationsPageSurfaceSource).toContain('<ReportingPanel />');
|
||||
expect(operationsPageSurfaceSource).toContain('<SystemLogsPanel />');
|
||||
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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue