From a866b27f2a7f038ae06d2c2a404ebfe16375b687 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:05:02 +0100 Subject: [PATCH 1/2] Hide Patrol navigation when AI is off and rename Updates Issue #905 asked for every AI entry point to disappear when AI is switched off, and the Assistant launcher still honours that through the assistantEnabled session capability. Patrol did not: v6 added it as a permanent top-level tab, mobile bottom-bar slot, g r shortcut and command-palette entry, and the palette also kept offering Assistant commands. Patrol cannot run without a configured provider, so an AI-off install carried a tab that only led to an "off" page. Gate all of those on the same capability as the Assistant launcher. Settings keeps its Patrol and Assistant items and /patrol stays reachable, so the path back to turning AI on is unchanged. Also rename the System settings item "Pulse server updates" to "Updates" and drop "runtime" from its description. The item already sits in the System group, and the description still sends agent updates to Infrastructure, which was the reason for the longer name. --- .../v6/internal/subsystems/ai-runtime.md | 15 ++++ .../v6/internal/subsystems/cloud-paid.md | 10 +++ .../subsystems/frontend-primitives.md | 3 +- .../internal/subsystems/security-privacy.md | 8 +++ .../internal/subsystems/storage-recovery.md | 8 +++ frontend-modern/browser-verification.json | 69 +++++++++++++------ frontend-modern/src/App.tsx | 2 + frontend-modern/src/AppLayout.tsx | 30 +++++--- .../src/__tests__/App.architecture.test.ts | 11 +++ .../src/__tests__/AppLayout.test.tsx | 16 +++++ .../__tests__/settingsArchitecture.test.ts | 8 +-- .../settingsHeaderMeta.branchcov0713.test.ts | 12 ++-- .../__tests__/settingsLocalization.test.ts | 2 +- .../__tests__/systemNavCapabilityGate.test.ts | 2 +- .../components/Settings/settingsHeaderMeta.ts | 4 +- .../components/Settings/settingsNavCatalog.ts | 2 +- .../shared/KeyboardShortcutsModal.tsx | 4 ++ .../__tests__/CommandPaletteModal.test.tsx | 18 +++++ .../__tests__/KeyboardShortcutsModal.test.tsx | 42 +++++++++++ .../components/shared/commandPaletteModel.ts | 44 ++++++------ .../shared/useCommandPaletteState.ts | 31 +++++---- .../src/hooks/useKeyboardShortcuts.ts | 5 +- frontend-modern/src/i18n/messages.de.ts | 6 +- frontend-modern/src/i18n/messages.es.ts | 6 +- frontend-modern/src/i18n/messages.ts | 6 +- frontend-modern/src/types/config.ts | 2 +- .../__tests__/updatesPresentation.test.ts | 5 +- .../src/utils/updatesPresentation.ts | 5 +- .../15-settings-shell-consistency.spec.ts | 4 +- 29 files changed, 281 insertions(+), 99 deletions(-) create mode 100644 frontend-modern/src/components/shared/__tests__/KeyboardShortcutsModal.test.tsx diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 17fa804828..c493f84f22 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 d83fda4213..4a22aee749 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 6c18bd3f05..0db2558293 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 d59d784af3..73a5bf5e52 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 242d23d134..0e49660db6 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 3d174cb47c..7b4f7f2758 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 4b34f08571..42a975de6d 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 33c7fb981b..19d5c9a972 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 1c7578ad04..dca733a7ee 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 67110a4f00..ebb7246b4c 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 7583fce747..969074349b 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 38b02f374e..6b48cbe8a5 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 eed8d0effa..886a986e98 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 40d5dbf748..22259cafb3 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 feeccc296b..408a27f97c 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 e08f016ef8..be83485a9d 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 fcaffbcc8c..02728edd6e 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 f82bf9db48..4c513a9665 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 8a54b5dc31..358d9e2558 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 29f5aded78..f85b0dfc9b 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 757cf801f2..58f133c9f8 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 97b677b6cb..8433dfe98e 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 2b0d784a3b..e3ae87ed7c 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 9c03d6d519..f6edd23d34 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 a7d1904bed..fcad328493 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 2a7cae4eca..761d375adc 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 86d1be85f1..10da5e0b64 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 3cd376ca2e..f119233d7a 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', From 1461f6486eeeedfd95b015780383f78d7eb86400 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:19:35 +0100 Subject: [PATCH 2/2] Stop describing Patrol modes on Actions while AI is off With AI switched off the empty Actions inbox still said "Patrol runs in Watch only mode" and pitched Pulse Pro modes, because the autonomy setting reads as monitor even when Patrol is not running at all. That tells an AI-off user something untrue and turns an empty page into an upsell for a feature they turned off (issue #905). The guidance now requires the same assistantEnabled session capability that gates the Assistant launcher and the Patrol navigation, and falls back to the plain empty-state copy otherwise. --- .../subsystems/frontend-primitives.md | 10 ++++ .../internal/subsystems/unified-resources.md | 8 +++ frontend-modern/browser-verification.json | 56 ++++--------------- .../actionsWatchOnlyEmptyState.test.ts | 9 +++ .../features/actions/actionPresentation.ts | 5 +- frontend-modern/src/pages/Actions.tsx | 2 + 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 0db2558293..d43728358d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -7461,3 +7461,13 @@ also follows the outdated-agent action to Agent Doctor with all 49 host IDs. The broader palette override should be corrected in its own shared-design slice, with background users migrated to explicit alpha utilities so other 900-level text consumers can use normal opaque color semantics. + +### Actions empty state follows the AI capability + +`getActionsWatchOnlyEmptyState` takes an explicit `aiEnabled` input, which +`pages/Actions.tsx` fills from the `assistantEnabled` session capability. While +AI is off it returns no guidance, so the empty Open inbox keeps its plain copy +instead of saying Patrol runs in Watch only mode or pitching Pro Patrol modes +for a feature that is not running (issue #905). With AI on, the Watch only, +switch and upgrade branches are unchanged. `actionsWatchOnlyEmptyState.test.ts` +pins both states and the page wiring. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 667324a2ef..954d9b0dc2 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -5396,3 +5396,11 @@ versus 4.953ms (+0.7%), with unchanged allocations. The 200-host merge was 4.7µs and 19 allocations. These paired runs remove the observed local ingest regression. The revised #2260 CI Benchmarks job passed on commit `bbce69ea65` with no benchmark regression gate failure. + +### Actions empty-state copy does not touch the action ledger + +The Actions page now hides its Patrol Watch only guidance while the +`assistantEnabled` session capability is false. This is empty-state +presentation only: the action audit list, approval counts, review dialog, +routing and every recorded action outcome are read and rendered exactly as +before, and no resource or action identity changes. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 7b4f7f2758..c6b080314d 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,69 +1,35 @@ { "version": 1, - "base_sha": "762aa6fe1a423f03138e8e7e533381f3e086e3c1", - "verified_at": "2026-09-28T21:05:00Z", + "base_sha": "a866b27f2a7f038ae06d2c2a404ebfe16375b687", + "verified_at": "2026-09-28T21:20: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/types/config.ts", - "frontend-modern/src/utils/updatesPresentation.ts" + "frontend-modern/src/features/actions/actionPresentation.ts", + "frontend-modern/src/pages/Actions.tsx" ], "content_sha256": { - "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" + "frontend-modern/src/features/actions/actionPresentation.ts": "8e67a99e744cf7dbbc1fc53da1d9697af78a90d95779df3553ee8867c3c51496", + "frontend-modern/src/pages/Actions.tsx": "88ab28e9333652e81770e570c140aa19f0b6d833be3b65fd41afd2e9614f60c5" }, "routes": [ - "/settings/system-updates", - "/patrol" + "/actions" ], "viewports": [ { "width": 1440, "height": 900 }, - { - "width": 800, - "height": 600 - }, { "width": 375, "height": 812 } ], "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" + "AI off (assistantEnabled false, non-mock backend) before the fix: empty Open tab read Patrol runs in Watch only mode with a Pulse Pro pitch", + "AI off after the fix: empty Open tab reads No actions need attention and Pulse will place proposed or in-progress changes here for review, at 1440px and 375px", + "AI on (mock backend): Open tab lists demo actions from Patrol, empty-state guidance not rendered" ], "interactions": [ - "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" + "Reloaded /actions at desktop and phone widths in both AI states" ] } diff --git a/frontend-modern/src/features/actions/__tests__/actionsWatchOnlyEmptyState.test.ts b/frontend-modern/src/features/actions/__tests__/actionsWatchOnlyEmptyState.test.ts index a2423ac2ac..7a54c48844 100644 --- a/frontend-modern/src/features/actions/__tests__/actionsWatchOnlyEmptyState.test.ts +++ b/frontend-modern/src/features/actions/__tests__/actionsWatchOnlyEmptyState.test.ts @@ -4,6 +4,7 @@ import actionsSource from '@/pages/Actions.tsx?raw'; import { getActionsWatchOnlyEmptyState } from '../actionPresentation'; const base = { + aiEnabled: true, patrolWatchOnly: true, patrolModesUnlocked: false, commercialSurfacesHidden: false, @@ -17,6 +18,14 @@ describe('getActionsWatchOnlyEmptyState', () => { expect(actionsSource).toContain('estimatedItemHeight={92}'); }); + it('returns nothing while AI is off, so Patrol is not described as running (#905)', () => { + expect(getActionsWatchOnlyEmptyState({ ...base, aiEnabled: false })).toBeUndefined(); + expect( + getActionsWatchOnlyEmptyState({ ...base, aiEnabled: false, patrolModesUnlocked: true }), + ).toBeUndefined(); + expect(actionsSource).toContain('aiEnabled: aiChatStore.enabled === true'); + }); + it('returns nothing when Patrol is not in Watch only', () => { expect(getActionsWatchOnlyEmptyState({ ...base, patrolWatchOnly: false })).toBeUndefined(); expect( diff --git a/frontend-modern/src/features/actions/actionPresentation.ts b/frontend-modern/src/features/actions/actionPresentation.ts index 6a00d0b319..20c60af45a 100644 --- a/frontend-modern/src/features/actions/actionPresentation.ts +++ b/frontend-modern/src/features/actions/actionPresentation.ts @@ -119,6 +119,9 @@ export const getActionApprovalBadgePresentation = ( }; export interface ActionsWatchOnlyEmptyStateInput { + // False while AI is off or unconfigured: Patrol is not running at all, so + // naming its Watch only mode (or pitching Pro modes) would be untrue. + aiEnabled: boolean; patrolWatchOnly: boolean; patrolModesUnlocked: boolean; commercialSurfacesHidden: boolean; @@ -136,7 +139,7 @@ export type ActionsWatchOnlyEmptyStatePresentation = export const getActionsWatchOnlyEmptyState = ( input: ActionsWatchOnlyEmptyStateInput, ): ActionsWatchOnlyEmptyStatePresentation | undefined => { - if (!input.patrolWatchOnly) return undefined; + if (!input.aiEnabled || !input.patrolWatchOnly) return undefined; if (input.patrolModesUnlocked) { return { kind: 'switch', diff --git a/frontend-modern/src/pages/Actions.tsx b/frontend-modern/src/pages/Actions.tsx index f87cb248c2..e808e68e63 100644 --- a/frontend-modern/src/pages/Actions.tsx +++ b/frontend-modern/src/pages/Actions.tsx @@ -10,6 +10,7 @@ import { Card } from '@/components/shared/Card'; import { UpgradeButtonLink } from '@/components/shared/UpgradeLink'; import { PATROL_AUTONOMY_FEATURE_KEY } from '@/features/patrol/patrolAutonomyAvailability'; import { PATROL_PATH } from '@/routing/resourceLinks'; +import { aiChatStore } from '@/stores/aiChat'; import { hasFeature } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { @@ -66,6 +67,7 @@ export function Actions() { const watchOnlyGuidance = createMemo(() => getActionsWatchOnlyEmptyState({ + aiEnabled: aiChatStore.enabled === true, patrolWatchOnly: patrolWatchOnly(), patrolModesUnlocked: hasFeature(PATROL_AUTONOMY_FEATURE_KEY), commercialSurfacesHidden: presentationPolicyHidesCommercialSurfaces(),