diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 17fa80482..c493f84f2 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -8469,3 +8469,18 @@ complete new snapshot, never a partially written file. This is an internal durability boundary, not a public API or stored-format change. The `TestKnowledgeStore_SaveLoad` cleanup join and the concurrent-save reload regression in `internal/ai/adapters/adapters_additional_test.go` enforce it. + +### AI entry points follow the assistant session capability + +Every AI entry point in the app shell follows the `assistantEnabled` session +capability (AI enabled and a provider configured, or mock mode). That covers +the Assistant launcher and Assistant command-palette commands, and the Patrol +top-level tab, mobile bottom-bar slot, `g r` shortcut, shortcuts-help row and +`Go to Patrol` palette command. Patrol cannot run without a configured +provider, so an AI-off install no longer carries navigation that only reaches +an "off" setup page, which restores the issue #905 behaviour for Patrol. The +Settings Patrol and Assistant items and the `/patrol` route stay reachable, so +turning AI back on needs no new path. This is presentation gating only: Patrol +runtime, findings, schedules and the capability's server derivation are +unchanged. `AppLayout.test.tsx`, `CommandPaletteModal.test.tsx`, +`KeyboardShortcutsModal.test.tsx` and `App.architecture.test.ts` pin it. diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index d83fda421..4a22aee74 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -3630,3 +3630,13 @@ activation semantics. The offline mobile audit explicitly activates the default context before it creates and activates isolated organizations, using the normal authenticated API. Its route/overflow checks and AppLayout regression cover this boundary without granting Community private RBAC or changing CI tier membership. + +### AI navigation gating stays outside commercial scope + +The app shell hides Patrol navigation and Assistant palette commands while the +`assistantEnabled` session capability is false. The gate reads only that AI +capability: it does not consult entitlements, organization scope, hosted +bootstrap or upgrade state, and it adds no commercial prompt. Paid Patrol +modes stay governed on the Patrol and Actions surfaces, which remain +reachable by route. `App.architecture.test.ts` pins that the gate reads the +same capability as the Assistant launcher. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 6c18bd3f0..0db255829 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -6874,7 +6874,8 @@ and ### Agent Doctor settings framing -Settings labels the application update panel **Pulse server updates** and keeps +Settings labels the application update panel **Updates** under the System +group, whose description sends agent updates to Infrastructure, and keeps agent lifecycle triage in the separate **Agent Doctor** dialog. Platform update notices, Diagnostics, and Infrastructure rows use the canonical Agent Doctor route handoff instead of recreating installer or repair controls. The dialog diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index d59d784af..73a5bf5e5 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -2906,3 +2906,11 @@ trust suite verifies every consumer pin, absence of the affected auth assumption workflow trust controls and the retained native Windows command/lifecycle proof steps. Native Windows execution remains a hosted check, not a local Linux claim. This upgrade is independent of the grouped signing/Docker/Tailscale updates. + +### Updates settings copy rename + +The System settings item formerly labelled "Pulse server updates" is now +"Updates" in English, German and Spanish, with a plain-language description +that still routes agent updates to Infrastructure. This is locale copy only: +the `system-updates` route id, its `systemSettingsRead` capability gate and +every update endpoint and authorization check are unchanged. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 242d23d13..0e49660db 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -6166,3 +6166,11 @@ walk the same group repeatedly during large-estate Storage navigation. The windowing test pins the row shape, and the 50-node browser fixture exercises expanded rows and History at desktop and phone widths. This is presentation work only; it changes no storage evidence, alert state or recovery authority. + +### App-shell AI navigation gating opens no recovery path + +`App.tsx` now passes the `assistantEnabled` session capability to the keyboard +shortcuts and the shortcuts help so Patrol entry points hide while AI is off. +The change is navigation presentation only. It adds no storage, backup, +snapshot or recovery surface, and it changes no route, authority or recovery +state. The Recovery settings item and its capability gate are untouched. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 3d174cb47..7b4f7f275 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,42 +1,69 @@ { "version": 1, - "base_sha": "ab4f04da3d67d20c51943666be9bf4515ebb3b3f", - "verified_at": "2026-09-28T17:08:52Z", + "base_sha": "762aa6fe1a423f03138e8e7e533381f3e086e3c1", + "verified_at": "2026-09-28T21:05:00Z", "result": "passed", "changed_paths": [ + "frontend-modern/src/App.tsx", + "frontend-modern/src/AppLayout.tsx", + "frontend-modern/src/components/Settings/settingsHeaderMeta.ts", + "frontend-modern/src/components/Settings/settingsNavCatalog.ts", + "frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx", + "frontend-modern/src/components/shared/commandPaletteModel.ts", + "frontend-modern/src/components/shared/useCommandPaletteState.ts", + "frontend-modern/src/hooks/useKeyboardShortcuts.ts", "frontend-modern/src/i18n/messages.de.ts", "frontend-modern/src/i18n/messages.es.ts", - "frontend-modern/src/i18n/messages.ts" + "frontend-modern/src/i18n/messages.ts", + "frontend-modern/src/types/config.ts", + "frontend-modern/src/utils/updatesPresentation.ts" ], "content_sha256": { - "frontend-modern/src/i18n/messages.de.ts": "4300b8dbbc163a1010dfd0c93d4d326b7ef05da02aedabbcfff393ba39b5f220", - "frontend-modern/src/i18n/messages.es.ts": "cd15c47444aaf7d823cd82b3b63cdad288a47da7bbb3393089eb8d3130f826a9", - "frontend-modern/src/i18n/messages.ts": "3e5f3057c10da70b92bc1c4343cede927c01979af7b1f8d00958d0c1d1ab60a0" + "frontend-modern/src/App.tsx": "a6ca67087cce917dc28914701262e6f631eb03621c24c8a773d5d025beb41215", + "frontend-modern/src/AppLayout.tsx": "d1d8bd884c43a20c1dbbde2ddb9f3278eb885b02c21e4cc3fa3fa1089cd1d320", + "frontend-modern/src/components/Settings/settingsHeaderMeta.ts": "d024df468000f183b2452d6b3fcb60fdce1b050107cd2e9d4ac586c4f13f2ddd", + "frontend-modern/src/components/Settings/settingsNavCatalog.ts": "8d3bff1c977b9db02c807d03fd80d40dabfc18c6a59809358c52a938a9c97c88", + "frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx": "a27ccd1bdccf31f1890779fc661ad4e896786594b99ff419160ce6c556fc7c96", + "frontend-modern/src/components/shared/commandPaletteModel.ts": "69a75c17579239c407ba18820e9f4d47d8b8b0e587cdcc3bb154a684b2a17047", + "frontend-modern/src/components/shared/useCommandPaletteState.ts": "95ff00c6e790f17fdd849e845b34e9047f655ec89ca279032ab764da283b4cd4", + "frontend-modern/src/hooks/useKeyboardShortcuts.ts": "f55d67c136d5d8e21ed220b33eac0512d5fae5b5aa835f60fa2570159c140d30", + "frontend-modern/src/i18n/messages.de.ts": "e55a115865e8b2c9cf4352eecac1a2f8996f4e0f51b9c14ddb7cfbe20e5b2f40", + "frontend-modern/src/i18n/messages.es.ts": "a348d63dd816e848cd75a0b5114b0f77c719d16117d9c86db719036fbe106af0", + "frontend-modern/src/i18n/messages.ts": "728aa1cf4b42698e14f16593825249c6a190c43d4ef28e2d4e28756c7c1990db", + "frontend-modern/src/types/config.ts": "49ad539bcefc2f87380b744c1da387401151e3f4af9047db8a85b1ce0b70322e", + "frontend-modern/src/utils/updatesPresentation.ts": "4696f8426385aaf287ab610746ca278acc7e23bd377c9739fcfedcf635910634" }, "routes": [ - "/preview/setup-complete?scenario=empty", - "/preview/setup-complete?scenario=vmware-api-backed" + "/settings/system-updates", + "/patrol" ], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1440, + "height": 900 }, { - "width": 390, - "height": 844 + "width": 800, + "height": 600 + }, + { + "width": 375, + "height": 812 } ], "states": [ - "Empty and API-connected setup-completion previews rendered in English, German and Spanish at both widths", - "Host installer and conditional temperature/SMART coverage copy displayed without horizontal overflow or uncaught page errors", - "Credential section collapsed after activating its disclosure in each of the twelve states" + "AI off (assistantEnabled false, non-mock backend): desktop System nav shows Alerts, Actions, Settings with no Patrol tab and no Assistant edge launcher", + "AI off at narrow width: mobile bottom bar shows Alerts, Actions, More with no Patrol slot", + "AI off: command palette lists Go to Alerts and Go to Settings only, no Patrol or Assistant commands", + "AI off: keyboard shortcuts help lists Go to Alerts and Go to Settings without Go to Patrol", + "AI on (mock backend, assistantEnabled true): desktop Patrol tab returns with attention count, Assistant launcher visible", + "AI on at 375px: mobile bar shows platform switcher, Alerts, Patrol, Actions, More", + "Settings System group reads General, Network, Updates, Recovery; page title Updates with plain-language description; Patrol and Assistant stay listed under Pulse Intelligence" ], "interactions": [ - "Opened empty and API-connected previews for each locale at desktop and narrow widths", - "Collapsed the credentials disclosure and verified the preview password was hidden" - ], - "command": "pulse-worker-browser web-setup-proof.cjs from the exact assigned workspace root; local Vite preview and Chromium 141.0.7390.37", - "artifacts": "/var/lib/pulse-maintainer/worker-outputs/web-product-zonkacnc/browser-proof-artifacts", - "notes": "No backend or installed release was exercised. Screenshots were inspected for English desktop, English API-connected narrow, German empty narrow and Spanish API-connected narrow; result.json records all twelve rendered states. The proof script is retained beside the artifacts." + "Opened the command palette with Cmd+K in both AI states and read the option list", + "Opened keyboard shortcuts help with ? while AI was off", + "Pressed g then r with AI off (stayed on /settings/system-updates) and with AI on (navigated to /patrol)", + "Resized between 1440x900 and 375x812 and reloaded the Updates and Patrol routes" + ] } diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 4b34f0857..42a975de6 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -460,6 +460,7 @@ function App() { useKeyboardShortcuts({ enabled: () => !runtime.needsAuth(), platformVisibility: platformNavigationVisibility, + patrolVisible: () => aiChatStore.enabled === true, isShortcutsOpen: shortcutsOpen, isCommandPaletteOpen: commandPaletteOpen, onToggleShortcuts: () => { @@ -592,6 +593,7 @@ function App() { isOpen={shortcutsOpen()} onClose={() => setShortcutsOpen(false)} platformVisibility={platformNavigationVisibility} + patrolVisible={() => aiChatStore.enabled === true} /> diff --git a/frontend-modern/src/AppLayout.tsx b/frontend-modern/src/AppLayout.tsx index 33c7fb981..19d5c9a97 100644 --- a/frontend-modern/src/AppLayout.tsx +++ b/frontend-modern/src/AppLayout.tsx @@ -603,17 +603,25 @@ export function AppLayout(props: AppLayoutProps) { breakdown, icon: BellIcon, }, - { - id: 'ai', - label: 'Patrol', - route: '/patrol', - tooltip: 'Review active operational attention and recent Patrol checks', - badge: null, - count: patrolAttentionCount() || undefined, - countLabel: patrolAttentionCountLabel(), - breakdown: undefined, - icon: PulsePatrolLogo, - }, + // Patrol cannot run without an enabled, configured AI provider, so its + // tab follows the same session capability as the Assistant launcher. + // Turning AI off in Settings hides every AI entry point (#905); the + // route itself stays reachable for links and the settings handoff. + ...(aiChatStore.enabled === true + ? [ + { + id: 'ai' as const, + label: 'Patrol', + route: '/patrol', + tooltip: 'Review active operational attention and recent Patrol checks', + badge: null, + count: patrolAttentionCount() || undefined, + countLabel: patrolAttentionCountLabel(), + breakdown: undefined, + icon: PulsePatrolLogo, + }, + ] + : []), { id: 'actions', label: 'Actions', diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index 1c7578ad0..dca733a7e 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -234,6 +234,17 @@ describe('Global update progress authorization', () => { }); describe('App architecture', () => { + it('gates Patrol navigation on the same AI capability as the Assistant launcher (#905)', () => { + expect(appSource).toContain('patrolVisible: () => aiChatStore.enabled === true'); + expect(appSource).toContain('patrolVisible={() => aiChatStore.enabled === true}'); + expect(appLayoutSource).toMatch( + /\.\.\.\(aiChatStore\.enabled === true\s*\?\s*\[\s*\{\s*id: 'ai' as const/, + ); + expect(appRuntimeStateSource).toContain( + 'aiChatStore.setEnabled(securityData?.sessionCapabilities?.assistantEnabled === true)', + ); + }); + it('keeps first-load connection progress understandable and retryable', () => { expect(appSource).toContain('fallback={}'); expect(appSource).not.toContain('
Loading...
'); diff --git a/frontend-modern/src/__tests__/AppLayout.test.tsx b/frontend-modern/src/__tests__/AppLayout.test.tsx index 67110a4f0..ebb7246b4 100644 --- a/frontend-modern/src/__tests__/AppLayout.test.tsx +++ b/frontend-modern/src/__tests__/AppLayout.test.tsx @@ -291,6 +291,22 @@ describe('AppLayout navigation icons', () => { expect(within(mobileNav).queryByText('Needs Attention')).toBeNull(); }); + it('hides the Patrol tab on desktop and mobile while AI is off (#905)', () => { + aiChatStore.setEnabled(false); + renderLayout(); + + const desktopNav = screen.getByRole('navigation', { name: 'Primary navigation' }); + const systemGroup = desktopNav.querySelector('[aria-label="System"]'); + expect(systemGroup).toBeTruthy(); + expect(within(systemGroup as HTMLElement).queryByRole('link', { name: 'Patrol' })).toBeNull(); + expect(within(systemGroup as HTMLElement).getByRole('link', { name: 'Actions' })).toBeTruthy(); + + const mobileNav = screen.getByRole('navigation', { name: 'Mobile navigation' }); + expect(mobileNav.querySelector('[data-tab-id="ai"]')).toBeNull(); + expect(mobileNav.querySelector('[data-tab-id="alerts"]')).toBeTruthy(); + expect(mobileNav.querySelector('[data-tab-id="actions"]')).toBeTruthy(); + }); + it('gives Actions its own navigation state and approval count', () => { actionInboxMockState.pendingActionCount = 3; renderLayout([], '/actions'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 7583fce74..969074349 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -429,7 +429,7 @@ describe('settings architecture guardrails', () => { it('gates the admin-only System tabs on the served systemSettingsRead capability', () => { // Same rationale as Infrastructure rather than the paid-feature one: a free // install can act on a paid tab by upgrading, but a non-admin cannot grant - // themselves admin, so Network / Pulse server updates / Recovery can only + // themselves admin, so Network / Updates / Recovery can only // end in a panel they will never populate. for (const id of ['system-network', 'system-updates', 'system-recovery']) { const navBlock = settingsNavCatalogSource.match( @@ -487,12 +487,12 @@ describe('settings architecture guardrails', () => { ); }); - it('keeps Pulse server updates separate from Agent Doctor lifecycle triage', () => { + it('keeps Pulse updates separate from Agent Doctor lifecycle triage', () => { const updatesNavBlock = settingsNavCatalogSource.match( /id: 'system-updates',[\s\S]*?id: 'system-recovery',/, ); - expect(updatesNavBlock?.[0]).toContain("label: 'Pulse server updates'"); - expect(settingsHeaderMetaSource).toContain("title: 'Pulse server updates'"); + expect(updatesNavBlock?.[0]).toContain("label: 'Updates'"); + expect(settingsHeaderMetaSource).toContain("title: 'Updates'"); expect(settingsNavCatalogSource).not.toContain("label: 'Agent Doctor'"); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts index 38b02f374..6b48cbe8a 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts @@ -118,9 +118,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-updates to the English baseline', () => { expect(en['system-updates']).toEqual({ - title: 'Pulse server updates', + title: 'Updates', description: - 'Manage Pulse server runtime version checks, update channels, and automatic updates. Agent updates stay under Infrastructure.', + 'Check for new Pulse versions, choose an update channel, and manage automatic updates. Agent updates stay under Infrastructure.', }); }); @@ -416,9 +416,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-updates', () => { expect(de['system-updates']).toEqual({ - title: 'Pulse-Server-Updates', + title: 'Updates', description: - 'Verwalten Sie Versionspruefungen, Update-Kanaele und automatische Updates der Pulse-Server-Laufzeit. Agent-Updates bleiben unter Infrastruktur.', + 'Suchen Sie nach neuen Pulse-Versionen, waehlen Sie einen Update-Kanal und verwalten Sie automatische Updates. Agent-Updates bleiben unter Infrastruktur.', }); }); @@ -612,9 +612,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-updates', () => { expect(es['system-updates']).toEqual({ - title: 'Actualizaciones del servidor Pulse', + title: 'Actualizaciones', description: - 'Administra las comprobaciones de versión, los canales y las actualizaciones automáticas del servidor Pulse. Las actualizaciones de agentes permanecen en Infraestructura.', + 'Busca nuevas versiones de Pulse, elige un canal de actualización y administra las actualizaciones automáticas. Las actualizaciones de agentes permanecen en Infraestructura.', }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts index eed8d0eff..886a986e9 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts @@ -34,7 +34,7 @@ describe('settings localization catalog', () => { const groups = getSettingsNavGroups('de'); expect(groups[0]?.label).toBe('Infrastruktur'); expect(groups[0]?.items[0]?.label).toBe('Infrastruktur'); - expect(getSettingsNavItem('system-updates', 'de')?.label).toBe('Pulse-Server-Updates'); + expect(getSettingsNavItem('system-updates', 'de')?.label).toBe('Updates'); expect(getSettingsNavItem('system-ai-assistant', 'de')?.label).toBe('Assistant'); expect(getSettingsNavItem('system-ai-discovery', 'de')).toBeUndefined(); expect(getSettingsNavItem('security-data-handling', 'de')?.label).toBe('Ressourcenschutz'); diff --git a/frontend-modern/src/components/Settings/__tests__/systemNavCapabilityGate.test.ts b/frontend-modern/src/components/Settings/__tests__/systemNavCapabilityGate.test.ts index 40d5dbf74..22259cafb 100644 --- a/frontend-modern/src/components/Settings/__tests__/systemNavCapabilityGate.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/systemNavCapabilityGate.test.ts @@ -6,7 +6,7 @@ import { type SettingsNavVisibilityContext, } from '../settingsNavVisibility'; -// System → Network, Pulse server updates, and Recovery are pure instance +// System → Network, Updates, and Recovery are pure instance // administration: the public URL and CORS boundaries, the server update // channel, and backup polling plus config export/import. Every route behind // them is RequireAdmin + settings:read, so a session without it was offered diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index feeccc296..408a27f97 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -25,9 +25,9 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { description: 'Configure the public URL, CORS, embedding, and webhook network boundaries.', }, 'system-updates': { - title: 'Pulse server updates', + title: 'Updates', description: - 'Manage Pulse server runtime version checks, update channels, and automatic updates. Agent updates stay under Infrastructure.', + 'Check for new Pulse versions, choose an update channel, and manage automatic updates. Agent updates stay under Infrastructure.', }, 'system-recovery': { title: 'Recovery', diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index e08f016ef..be83485a9 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -190,7 +190,7 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ }, { id: 'system-updates', - label: 'Pulse server updates', + label: 'Updates', icon: RefreshCw, iconProps: { strokeWidth: 2 }, saveBehavior: 'system', diff --git a/frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx b/frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx index fcaffbcc8..02728edd6 100644 --- a/frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx +++ b/frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx @@ -14,6 +14,7 @@ interface KeyboardShortcutsModalProps { isOpen: boolean; onClose: () => void; platformVisibility: () => PlatformNavigationVisibility; + patrolVisible: () => boolean; } const UNIFIED_NAV_SHORTCUTS: ShortcutGroup = { @@ -31,6 +32,8 @@ const UNIFIED_NAV_SHORTCUTS: ShortcutGroup = { ], }; +const PATROL_NAV_SHORTCUT_KEYS = 'g then r'; + const NAV_PRIMARY_SHORTCUTS: Record = { 'g then s': 'standalone', 'g then p': 'proxmox', @@ -54,6 +57,7 @@ export function KeyboardShortcutsModal(props: KeyboardShortcutsModalProps) { const shortcutGroups = createMemo(() => { const platformVisibility = props.platformVisibility(); const visibleNavigationItems = UNIFIED_NAV_SHORTCUTS.items.filter((item) => { + if (item.keys === PATROL_NAV_SHORTCUT_KEYS) return props.patrolVisible(); const navId = NAV_PRIMARY_SHORTCUTS[item.keys]; if (!navId) return true; return primaryPlatformNavigationIsVisible(platformVisibility, navId); diff --git a/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx b/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx index f82bf9db4..4c513a966 100644 --- a/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx @@ -50,10 +50,12 @@ describe('CommandPaletteModal', () => { cleanup(); navigateMock.mockReset(); vi.restoreAllMocks(); + aiChatStore.setEnabled(false); }); beforeEach(() => { window.history.replaceState({}, '', '/proxmox/overview'); + aiChatStore.setEnabled(true); }); it('keeps the command palette on shell, runtime, and model owners', () => { @@ -479,6 +481,22 @@ describe('CommandPaletteModal', () => { expect(onClose).toHaveBeenCalledTimes(1); }); + it('offers neither Patrol nor Assistant commands while AI is off (#905)', () => { + aiChatStore.setEnabled(false); + render(() => ( + + )); + + expect(screen.getByText('Go to Alerts')).toBeInTheDocument(); + expect(screen.getByText('Go to Settings')).toBeInTheDocument(); + expect(screen.queryByText('Go to Patrol')).not.toBeInTheDocument(); + expect(screen.queryByText('Show Assistant commands')).not.toBeInTheDocument(); + }); + it('hides platform commands without supported infrastructure evidence', () => { render(() => ( ({ + Dialog: (props: { isOpen: boolean; children: JSX.Element }) => + props.isOpen ?
{props.children}
: null, +})); + +import { KeyboardShortcutsModal } from '@/components/shared/KeyboardShortcutsModal'; + +describe('KeyboardShortcutsModal', () => { + afterEach(() => { + cleanup(); + }); + + const renderModal = (patrolVisible: boolean) => + render(() => ( + patrolVisible} + /> + )); + + it('lists the Patrol shortcut while Patrol navigation is visible', () => { + renderModal(true); + + expect(screen.getByText('Go to Patrol')).toBeInTheDocument(); + expect(screen.getByText('Go to Alerts')).toBeInTheDocument(); + }); + + it('drops the Patrol shortcut while AI is off (#905)', () => { + renderModal(false); + + expect(screen.queryByText('Go to Patrol')).not.toBeInTheDocument(); + expect(screen.getByText('Go to Alerts')).toBeInTheDocument(); + expect(screen.getByText('Go to Settings')).toBeInTheDocument(); + }); +}); diff --git a/frontend-modern/src/components/shared/commandPaletteModel.ts b/frontend-modern/src/components/shared/commandPaletteModel.ts index 8a54b5dc3..358d9e255 100644 --- a/frontend-modern/src/components/shared/commandPaletteModel.ts +++ b/frontend-modern/src/components/shared/commandPaletteModel.ts @@ -49,6 +49,8 @@ export type CommandPaletteAssistantOpenPresentation = { export function buildCommandPaletteCommands(options: { paths: CommandPaletteCommandPaths; platformVisibility: PlatformNavigationVisibility; + // Omitted means visible, for callers that do not gate Patrol. + patrolVisible?: boolean; navigate: (path: string) => void; assistantActions?: CommandPaletteAssistantActions; assistantOpenPresentation?: CommandPaletteAssistantOpenPresentation; @@ -242,32 +244,34 @@ export function buildCommandPaletteCommands(options: { }); } - commands.push( - { - id: 'nav-alerts', - label: 'Go to Alerts', - description: '/alerts', - shortcut: 'g a', - keywords: ['alarms', 'notifications'], - action: () => options.navigate('/alerts'), - }, - { + commands.push({ + id: 'nav-alerts', + label: 'Go to Alerts', + description: '/alerts', + shortcut: 'g a', + keywords: ['alarms', 'notifications'], + action: () => options.navigate('/alerts'), + }); + + if (options.patrolVisible !== false) { + commands.push({ id: 'nav-patrol', label: 'Go to Patrol', description: '/patrol', shortcut: 'g r', keywords: ['needs attention', 'patrol', 'findings', 'ai', 'verification'], action: () => options.navigate('/patrol'), - }, - { - id: 'nav-settings', - label: 'Go to Settings', - description: '/settings', - shortcut: 'g t', - keywords: ['preferences', 'config'], - action: () => options.navigate('/settings'), - }, - ); + }); + } + + commands.push({ + id: 'nav-settings', + label: 'Go to Settings', + description: '/settings', + shortcut: 'g t', + keywords: ['preferences', 'config'], + action: () => options.navigate('/settings'), + }); return commands; } diff --git a/frontend-modern/src/components/shared/useCommandPaletteState.ts b/frontend-modern/src/components/shared/useCommandPaletteState.ts index 29f5aded7..f85b0dfc9 100644 --- a/frontend-modern/src/components/shared/useCommandPaletteState.ts +++ b/frontend-modern/src/components/shared/useCommandPaletteState.ts @@ -36,6 +36,10 @@ export function useCommandPaletteState(props: CommandPaletteModalProps) { runAfterPaletteSelection(() => aiChatStore.requestCommand(action)); }; + // Same session capability that gates the Assistant launcher and the Patrol + // tab: with AI off, the palette offers neither Assistant nor Patrol. + const aiAvailable = () => aiChatStore.enabled === true; + const commands = createMemo(() => buildCommandPaletteCommands({ paths: { @@ -49,23 +53,26 @@ export function useCommandPaletteState(props: CommandPaletteModalProps) { vmwareNetworksPath: buildVmwarePath('networks'), }, platformVisibility: props.platformVisibility(), + patrolVisible: aiAvailable(), navigate, assistantOpenPresentation: { label: assistantPageContext().commandLabel, description: assistantPageContext().commandDescription, }, - assistantActions: { - open: () => - runAfterPaletteSelection(() => aiChatStore.open(assistantPageContext().context)), - help: () => requestAssistantCommand('help'), - newSession: () => requestAssistantCommand('new'), - sessions: () => requestAssistantCommand('sessions'), - models: () => requestAssistantCommand('models'), - providers: () => requestAssistantCommand('providers'), - status: () => requestAssistantCommand('status'), - undo: () => requestAssistantCommand('undo'), - redo: () => requestAssistantCommand('redo'), - }, + assistantActions: aiAvailable() + ? { + open: () => + runAfterPaletteSelection(() => aiChatStore.open(assistantPageContext().context)), + help: () => requestAssistantCommand('help'), + newSession: () => requestAssistantCommand('new'), + sessions: () => requestAssistantCommand('sessions'), + models: () => requestAssistantCommand('models'), + providers: () => requestAssistantCommand('providers'), + status: () => requestAssistantCommand('status'), + undo: () => requestAssistantCommand('undo'), + redo: () => requestAssistantCommand('redo'), + } + : undefined, }), ); diff --git a/frontend-modern/src/hooks/useKeyboardShortcuts.ts b/frontend-modern/src/hooks/useKeyboardShortcuts.ts index 757cf801f..58f133c9f 100644 --- a/frontend-modern/src/hooks/useKeyboardShortcuts.ts +++ b/frontend-modern/src/hooks/useKeyboardShortcuts.ts @@ -27,6 +27,9 @@ type KeyboardShortcutsOptions = { onToggleCommandPalette?: () => void; onFocusSearch?: () => boolean | void; platformVisibility?: Accessor; + // Patrol navigation exists only while AI is enabled and configured; an + // absent accessor keeps the shortcut for callers that do not gate it. + patrolVisible?: Accessor; }; const isEditableTarget = (target: EventTarget | null): boolean => { @@ -99,7 +102,7 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) { return { ...infrastructureRoutes, a: '/alerts', - r: '/patrol', + ...(options.patrolVisible?.() === false ? {} : { r: '/patrol' }), t: '/settings', }; }; diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index 97b677b6c..8433dfe98 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -491,8 +491,8 @@ export const DE_MESSAGE_OVERRIDES = { 'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.', 'settings.header.systemRelay.title': 'Remote-Zugriff', 'settings.header.systemUpdates.description': - 'Verwalten Sie Versionspruefungen, Update-Kanaele und automatische Updates der Pulse-Server-Laufzeit. Agent-Updates bleiben unter Infrastruktur.', - 'settings.header.systemUpdates.title': 'Pulse-Server-Updates', + 'Suchen Sie nach neuen Pulse-Versionen, waehlen Sie einen Update-Kanal und verwalten Sie automatische Updates. Agent-Updates bleiben unter Infrastruktur.', + 'settings.header.systemUpdates.title': 'Updates', 'settings.header.supportDiagnostics.description': 'Fuehren Sie Zustandspruefungen aus, validieren Sie Verbindungen und exportieren Sie Troubleshooting-Snapshots.', 'settings.header.supportDiagnostics.title': 'Diagnose & Zustand', @@ -536,7 +536,7 @@ export const DE_MESSAGE_OVERRIDES = { 'settings.nav.item.sharing': 'Freigabe', 'settings.nav.item.singleSignOn': 'Single Sign-On', 'settings.nav.item.systemLogs': 'Systemprotokolle', - 'settings.nav.item.updates': 'Pulse-Server-Updates', + 'settings.nav.item.updates': 'Updates', 'settings.nav.item.users': 'Benutzer', 'settings.shell.collapseSidebarLabel': 'Einstellungsnavigation einklappen', 'settings.shell.configurationLoading': 'Konfiguration wird geladen...', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index 2b0d784a3..e3ae87ed7 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -484,8 +484,8 @@ export const ES_MESSAGE_OVERRIDES = { 'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.', 'settings.header.systemRelay.title': 'Acceso remoto', 'settings.header.systemUpdates.description': - 'Administra las comprobaciones de versión, los canales y las actualizaciones automáticas del servidor Pulse. Las actualizaciones de agentes permanecen en Infraestructura.', - 'settings.header.systemUpdates.title': 'Actualizaciones del servidor Pulse', + 'Busca nuevas versiones de Pulse, elige un canal de actualización y administra las actualizaciones automáticas. Las actualizaciones de agentes permanecen en Infraestructura.', + 'settings.header.systemUpdates.title': 'Actualizaciones', 'settings.header.supportDiagnostics.description': 'Ejecuta comprobaciones de salud, valida conectividad y exporta snapshots de resolución de problemas.', 'settings.header.supportDiagnostics.title': 'Diagnóstico y salud', @@ -529,7 +529,7 @@ export const ES_MESSAGE_OVERRIDES = { 'settings.nav.item.sharing': 'Uso compartido', 'settings.nav.item.singleSignOn': 'Inicio de sesión único', 'settings.nav.item.systemLogs': 'Logs del sistema', - 'settings.nav.item.updates': 'Actualizaciones del servidor Pulse', + 'settings.nav.item.updates': 'Actualizaciones', 'settings.nav.item.users': 'Usuarios', 'settings.shell.collapseSidebarLabel': 'Contraer navegación de ajustes', 'settings.shell.configurationLoading': 'Cargando configuración...', diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index 9c03d6d51..f6edd23d3 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -474,8 +474,8 @@ export const EN_MESSAGES = { 'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.', 'settings.header.systemRelay.title': 'Remote Access', 'settings.header.systemUpdates.description': - 'Manage Pulse server runtime version checks, update channels, and automatic updates. Agent updates stay under Infrastructure.', - 'settings.header.systemUpdates.title': 'Pulse server updates', + 'Check for new Pulse versions, choose an update channel, and manage automatic updates. Agent updates stay under Infrastructure.', + 'settings.header.systemUpdates.title': 'Updates', 'settings.header.supportDiagnostics.description': 'Run health checks, validate connectivity, and export troubleshooting snapshots.', 'settings.header.supportDiagnostics.title': 'Diagnostics & Health', @@ -519,7 +519,7 @@ export const EN_MESSAGES = { 'settings.nav.item.sharing': 'Sharing', 'settings.nav.item.singleSignOn': 'Single Sign-On', 'settings.nav.item.systemLogs': 'System Logs', - 'settings.nav.item.updates': 'Pulse server updates', + 'settings.nav.item.updates': 'Updates', 'settings.nav.item.users': 'Users', 'settings.shell.collapseSidebarLabel': 'Collapse settings navigation', 'settings.shell.configurationLoading': 'Loading configuration...', diff --git a/frontend-modern/src/types/config.ts b/frontend-modern/src/types/config.ts index a7d1904be..fcad32849 100644 --- a/frontend-modern/src/types/config.ts +++ b/frontend-modern/src/types/config.ts @@ -105,7 +105,7 @@ export interface SecurityStatusSettingsCapabilities { reportingRead: boolean; /** * The same RequireAdmin + settings:read gate as infrastructureRead, scoped to - * the System > Network / Pulse server updates / Recovery tabs. A sibling + * the System > Network / Updates / Recovery tabs. A sibling * rather than a reuse, so tightening one surface's gate cannot silently hide * the other's tabs. */ diff --git a/frontend-modern/src/utils/__tests__/updatesPresentation.test.ts b/frontend-modern/src/utils/__tests__/updatesPresentation.test.ts index 2a7cae4ec..761d375ad 100644 --- a/frontend-modern/src/utils/__tests__/updatesPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/updatesPresentation.test.ts @@ -21,9 +21,8 @@ describe('updatesPresentation', () => { it('returns canonical updates panel framing copy', () => { expect(UPDATES_PANEL_COPY).toEqual({ - title: 'Pulse server updates', - description: - 'Manage the Pulse server runtime. Pulse Agent updates are diagnosed under Infrastructure.', + title: 'Updates', + description: 'Keep Pulse up to date. Agent updates are diagnosed under Infrastructure.', currentVersionLabel: 'Server version', checkNowLabel: 'Check Now', checkingLabel: 'Checking...', diff --git a/frontend-modern/src/utils/updatesPresentation.ts b/frontend-modern/src/utils/updatesPresentation.ts index 86d1be85f..10da5e0b6 100644 --- a/frontend-modern/src/utils/updatesPresentation.ts +++ b/frontend-modern/src/utils/updatesPresentation.ts @@ -7,9 +7,8 @@ export interface UpdateBuildBadge { } export const UPDATES_PANEL_COPY = { - title: 'Pulse server updates', - description: - 'Manage the Pulse server runtime. Pulse Agent updates are diagnosed under Infrastructure.', + title: 'Updates', + description: 'Keep Pulse up to date. Agent updates are diagnosed under Infrastructure.', currentVersionLabel: 'Server version', checkNowLabel: 'Check Now', checkingLabel: 'Checking...', diff --git a/tests/integration/tests/15-settings-shell-consistency.spec.ts b/tests/integration/tests/15-settings-shell-consistency.spec.ts index 3cd376ca2..f119233d7 100644 --- a/tests/integration/tests/15-settings-shell-consistency.spec.ts +++ b/tests/integration/tests/15-settings-shell-consistency.spec.ts @@ -61,9 +61,9 @@ const SETTINGS_SHELL_ROUTES = [ }, { route: '/settings/system-updates', - title: 'Pulse server updates', + title: 'Updates', description: - 'Manage Pulse server runtime version checks, update channels, and automatic updates. Agent updates stay under Infrastructure.', + 'Check for new Pulse versions, choose an update channel, and manage automatic updates. Agent updates stay under Infrastructure.', }, { route: '/settings/system-recovery',