mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 20:52:51 +00:00
Integrate AI-off navigation and Actions copy with main repairs
Compose protected Pulse PRs #2312 and #2313 with the reviewed main candidate so first-source setup guidance, the corrected E2E assertion, and later repairs can be verified together. Browser proof for the composed source follows in a separate receipt commit. Change-source: pulse-maintainer
This commit is contained in:
@@ -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
|
||||
@@ -7460,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "ab4f04da3d67d20c51943666be9bf4515ebb3b3f",
|
||||
"verified_at": "2026-09-28T17:08:52Z",
|
||||
"base_sha": "a866b27f2a7f038ae06d2c2a404ebfe16375b687",
|
||||
"verified_at": "2026-09-28T21:20:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/i18n/messages.de.ts",
|
||||
"frontend-modern/src/i18n/messages.es.ts",
|
||||
"frontend-modern/src/i18n/messages.ts"
|
||||
"frontend-modern/src/features/actions/actionPresentation.ts",
|
||||
"frontend-modern/src/pages/Actions.tsx"
|
||||
],
|
||||
"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/features/actions/actionPresentation.ts": "8e67a99e744cf7dbbc1fc53da1d9697af78a90d95779df3553ee8867c3c51496",
|
||||
"frontend-modern/src/pages/Actions.tsx": "88ab28e9333652e81770e570c140aa19f0b6d833be3b65fd41afd2e9614f60c5"
|
||||
},
|
||||
"routes": [
|
||||
"/preview/setup-complete?scenario=empty",
|
||||
"/preview/setup-complete?scenario=vmware-api-backed"
|
||||
"/actions"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
"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) 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 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."
|
||||
"Reloaded /actions at desktop and phone widths in both AI states"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
});
|
||||
|
||||
|
||||
+6
-6
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user