mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
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:
parent
762aa6fe1a
commit
a866b27f2a
29 changed files with 281 additions and 99 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue