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.
This commit is contained in:
rcourtman 2026-09-28 22:05:02 +01:00
parent 762aa6fe1a
commit a866b27f2a
29 changed files with 281 additions and 99 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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"
]
}

View file

@ -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}
/>
</Show>
<Show when={commandPaletteOpen()}>

View file

@ -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',

View file

@ -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={<AppBootstrapStatus />}');
expect(appSource).not.toContain('<div class="text-muted">Loading...</div>');

View file

@ -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');

View file

@ -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'");
});

View file

@ -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.',
});
});

View file

@ -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');

View file

@ -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

View file

@ -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',

View file

@ -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',

View file

@ -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<string, keyof PlatformNavigationVisibility> = {
'g then s': 'standalone',
'g then p': 'proxmox',
@ -54,6 +57,7 @@ export function KeyboardShortcutsModal(props: KeyboardShortcutsModalProps) {
const shortcutGroups = createMemo<ShortcutGroup[]>(() => {
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);

View file

@ -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(() => (
<CommandPaletteModal
isOpen={true}
onClose={vi.fn()}
platformVisibility={platformVisibility}
/>
));
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(() => (
<CommandPaletteModal

View file

@ -0,0 +1,42 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen } from '@solidjs/testing-library';
import type { JSX } from 'solid-js';
import { createEmptyPlatformNavigationVisibility } from '@/features/platformNavigation/platformNavigationModel';
vi.mock('@/components/shared/Dialog', () => ({
Dialog: (props: { isOpen: boolean; children: JSX.Element }) =>
props.isOpen ? <div>{props.children}</div> : null,
}));
import { KeyboardShortcutsModal } from '@/components/shared/KeyboardShortcutsModal';
describe('KeyboardShortcutsModal', () => {
afterEach(() => {
cleanup();
});
const renderModal = (patrolVisible: boolean) =>
render(() => (
<KeyboardShortcutsModal
isOpen={true}
onClose={vi.fn()}
platformVisibility={createEmptyPlatformNavigationVisibility}
patrolVisible={() => 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();
});
});

View file

@ -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;
}

View file

@ -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,
}),
);

View file

@ -27,6 +27,9 @@ type KeyboardShortcutsOptions = {
onToggleCommandPalette?: () => void;
onFocusSearch?: () => boolean | void;
platformVisibility?: Accessor<PlatformNavigationVisibility>;
// 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<boolean>;
};
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',
};
};

View file

@ -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...',

View file

@ -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...',

View file

@ -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...',

View file

@ -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.
*/

View file

@ -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...',

View file

@ -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...',

View file

@ -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',