diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index a3c0fc5bbc..95c88e7d3d 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -148,7 +148,7 @@ export const DiscoveryTab: Component = (props) => {
-

Service Context Disabled

+

Service identification is off

Enable service context to inspect what is running on this resource.

diff --git a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx index 6bb2597fda..5abd1da440 100644 --- a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx +++ b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx @@ -88,7 +88,7 @@ describe('DiscoveryTab', () => { )); - expect(await screen.findByText('Service Context Disabled')).toBeInTheDocument(); + expect(await screen.findByText('Service identification is off')).toBeInTheDocument(); expect(discoveryApi.getDiscovery).not.toHaveBeenCalled(); expect(discoveryApi.getDiscoveryInfo).not.toHaveBeenCalled(); expect(discoveryApi.getConnectedAgents).not.toHaveBeenCalled(); diff --git a/frontend-modern/src/components/Discovery/discoveryReadiness.ts b/frontend-modern/src/components/Discovery/discoveryReadiness.ts index 657bac91d4..914bbd0525 100644 --- a/frontend-modern/src/components/Discovery/discoveryReadiness.ts +++ b/frontend-modern/src/components/Discovery/discoveryReadiness.ts @@ -15,7 +15,7 @@ export type DiscoveryReadinessStatus = | 'ready'; export interface DiscoveryReadinessInputs { - /** The service context toggle (Settings -> Pulse Intelligence -> Service Context). */ + /** The service context toggle (Settings -> Pulse Intelligence -> Assistant). */ discoveryEnabled: boolean; /** Whether at least one AI provider has credentials configured. */ aiProviderConfigured: boolean; diff --git a/frontend-modern/src/components/Discovery/useDiscoveryTabState.ts b/frontend-modern/src/components/Discovery/useDiscoveryTabState.ts index d993b5fb01..235c401cfd 100644 --- a/frontend-modern/src/components/Discovery/useDiscoveryTabState.ts +++ b/frontend-modern/src/components/Discovery/useDiscoveryTabState.ts @@ -198,9 +198,7 @@ export function useDiscoveryTabState(props: DiscoveryTabStateProps) { const handleTriggerDiscovery = async (force = false) => { if (!discoveryFeatureEnabled()) { - setScanError( - 'Service context is disabled in Settings -> Pulse Intelligence -> Service Context.', - ); + setScanError('Service context is disabled in Settings -> Pulse Intelligence -> Assistant.'); return; } diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index a1a3697167..2731e0e47f 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -38,6 +38,7 @@ interface ResourceDetailDrawerProps { presentation?: ResourceDetailDrawerPresentation; resolveResourceLabel?: (resourceId: string) => string | null | undefined; initialShowAccessContext?: boolean; + initialShowHostDetails?: boolean; initialShowTrueNASDetails?: boolean; onResourceActionSettled?: () => void | Promise; } @@ -69,6 +70,7 @@ const DrawerContent: Component = (props) => { presentation: presentation(), resolveResourceLabel: props.resolveResourceLabel, initialShowAccessContext: props.initialShowAccessContext, + initialShowHostDetails: props.initialShowHostDetails, initialShowTrueNASDetails: props.initialShowTrueNASDetails, }); const headingId = () => `resource-detail-drawer-heading-${props.resource.id}`; @@ -354,6 +356,7 @@ export const ResourceDetailDrawer: Component = (props presentation={props.presentation} resolveResourceLabel={props.resolveResourceLabel} initialShowAccessContext={props.initialShowAccessContext} + initialShowHostDetails={props.initialShowHostDetails} initialShowTrueNASDetails={props.initialShowTrueNASDetails} onResourceActionSettled={props.onResourceActionSettled} /> diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts index 1653d7c129..67fd282815 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts @@ -358,14 +358,14 @@ describe('toDiscoveryConfig', () => { }); describe('resource drawer discovery promotion', () => { - it('points disabled discovery readiness at Pulse Intelligence service context settings', () => { - expect(discoveryTabSource).toContain('Service Context Disabled'); + it('points disabled discovery readiness at Assistant-owned service identification settings', () => { + expect(discoveryTabSource).toContain('Service identification is off'); expect(discoveryTabSource).toContain('getDiscoveryServiceContextSettingsTarget'); expect(discoveryTabSource).toContain('serviceContextSettingsTarget.label'); expect(discoveryTabStateSource).toContain( - 'Service context is disabled in Settings -> Pulse Intelligence -> Service Context.', + 'Service context is disabled in Settings -> Pulse Intelligence -> Assistant.', ); - expect(discoveryReadinessSource).toContain('Settings -> Pulse Intelligence -> Service Context'); + expect(discoveryReadinessSource).toContain('Settings -> Pulse Intelligence -> Assistant'); }); it('does not treat command-only diagnostic records as meaningful resource context', () => { diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts index e6df42dd18..9905933fa1 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts @@ -17,20 +17,14 @@ import { buildResourceAssistantContext } from '@/utils/resourceAssistantContextM import type { ResourceDetailDrawerPresentation } from './resourceDetailDrawerPresentation'; type DrawerTab = - | 'overview' - | 'history' - | 'discovery' - | 'mail' - | 'namespaces' - | 'deployments' - | 'swarm' - | 'debug'; + 'overview' | 'history' | 'discovery' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; export interface UseResourceDetailDrawerStateOptions { resource: Resource; resolveResourceLabel?: (resourceId: string) => string | null | undefined; presentation?: ResourceDetailDrawerPresentation; initialShowAccessContext?: boolean; + initialShowHostDetails?: boolean; initialShowTrueNASDetails?: boolean; } @@ -52,7 +46,8 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta const [showInvestigationContext, setShowInvestigationContext] = createSignal(false); const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false); const [showHostDetails, setShowHostDetails] = createSignal( - options.presentation === 'table-row' && isPulseAgentPlatformResource(resource), + options.initialShowHostDetails ?? + (options.presentation === 'table-row' && isPulseAgentPlatformResource(resource)), ); const [showServiceDetails, setShowServiceDetails] = createSignal(false); const [showVMwareDetails, setShowVMwareDetails] = createSignal(false); diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index f1b0702d86..ab40d4a52a 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -267,6 +267,22 @@ const AssistantSettingsContent: Component<{ state: ReturnType +
+ + Service identification + + Model-backed context used by Assistant and Patrol + + +
+

+ Identify service facts that help Assistant and Patrol explain monitored resources. + Infrastructure discovery and onboarding remain under Infrastructure. +

+ + +
+
); diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 30d680bd71..5038f1db49 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -1,4 +1,12 @@ -import { Component, createSignal, onMount, Show, Suspense, createMemo } from 'solid-js'; +import { + Component, + createEffect, + createSignal, + onMount, + Show, + Suspense, + createMemo, +} from 'solid-js'; import { Dynamic } from 'solid-js/web'; import { useNavigate, useLocation } from '@solidjs/router'; import { useWebSocket } from '@/contexts/appRuntime'; @@ -147,6 +155,11 @@ const SettingsWorkspace: Component = (props) => { return settingsPanelRegistry()[currentTab]; }); + createEffect(() => { + activeTab(); + queueMicrotask(() => window.scrollTo({ top: 0, behavior: 'auto' })); + }); + onMount(() => { void loadRuntimeCapabilities(); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts index 1a017e87e5..1699fdb2c0 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts @@ -34,7 +34,8 @@ describe('settings localization catalog', () => { expect(groups[0]?.label).toBe('Infrastruktur'); expect(groups[0]?.items[0]?.label).toBe('Infrastruktur'); expect(getSettingsNavItem('system-updates', 'de')?.label).toBe('Updates'); - expect(getSettingsNavItem('system-ai-discovery', 'de')?.label).toBe('Service-Kontext'); + 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'); }); @@ -44,7 +45,7 @@ describe('settings localization catalog', () => { expect(getSettingsNavItem('system-ai', 'es')?.label).toBe('Proveedores y modelos'); expect(getSettingsNavItem('system-ai-patrol', 'es')?.label).toBe('Patrol'); expect(getSettingsNavItem('system-ai-assistant', 'es')?.label).toBe('Assistant'); - expect(getSettingsNavItem('system-ai-discovery', 'es')?.label).toBe('Contexto de servicio'); + expect(getSettingsNavItem('system-ai-discovery', 'es')).toBeUndefined(); expect(getSettingsNavItem('support-diagnostics', 'es')?.label).toBe('Diagnóstico y salud'); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx index 92f30c7c7e..511976923d 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -394,13 +394,17 @@ describe('settingsNavigation integration scaffold', () => { for (const [tab, path] of Object.entries(dispatchableCanonicalTabPaths) as Array< [SettingsTab, string] >) { - expect(deriveTabFromPath(path)).toBe(tab); + expect(deriveTabFromPath(path)).toBe( + tab === 'system-ai-discovery' ? 'system-ai-assistant' : tab, + ); } }); it('round-trips settingsTabPath through deriveTabFromPath', () => { for (const tab of Object.keys(dispatchableCanonicalTabPaths) as SettingsTab[]) { - expect(deriveTabFromPath(settingsTabPath(tab))).toBe(tab); + expect(deriveTabFromPath(settingsTabPath(tab))).toBe( + tab === 'system-ai-discovery' ? 'system-ai-assistant' : tab, + ); } }); @@ -414,7 +418,7 @@ describe('settingsNavigation integration scaffold', () => { for (const tab of Object.keys(dispatchableCanonicalTabPaths) as SettingsTab[]) { const path = settingsTabPath(tab); const derived = deriveTabFromPath(path); - expect(derived).toBe(tab); + expect(derived).toBe(tab === 'system-ai-discovery' ? 'system-ai-assistant' : tab); } }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts index c678ad271e..19b8bc2573 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsRouting.test.ts @@ -205,7 +205,7 @@ describe('settingsNavigationModel', () => { ['?tab=system-ai', 'system-ai'], ['?tab=patrol', 'system-ai-patrol'], ['?tab=assistant', 'system-ai-assistant'], - ['?tab=discovery', 'system-ai-discovery'], + ['?tab=discovery', 'system-ai-assistant'], ['?ai_oauth_error=unsupported', 'system-ai'], ['?ai_oauth_success=true', 'system-ai'], ['?tab=system-relay', 'system-relay'], diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index 8494aaf7bf..32a22f2f65 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -81,12 +81,6 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ icon: Terminal, iconProps: { strokeWidth: 2 }, }, - { - id: 'system-ai-discovery', - label: 'Service Context', - icon: Network, - iconProps: { strokeWidth: 2 }, - }, ], }, { diff --git a/frontend-modern/src/components/Settings/settingsNavigationModel.ts b/frontend-modern/src/components/Settings/settingsNavigationModel.ts index 3ff1fd9ded..d3eb52727c 100644 --- a/frontend-modern/src/components/Settings/settingsNavigationModel.ts +++ b/frontend-modern/src/components/Settings/settingsNavigationModel.ts @@ -196,6 +196,9 @@ export function resolveCanonicalSettingsPath(path: string): string | null { ) { return settingsTabPath('system-ai'); } + if (normalizedPath === PULSE_INTELLIGENCE_DISCOVERY_PREFIX) { + return settingsTabPath('system-ai-assistant'); + } if (normalizedPath === SUPPORT_PREFIX) { return SUPPORT_DIAGNOSTICS_PREFIX; } @@ -327,7 +330,7 @@ export function deriveTabFromQuery(search: string): SettingsTab | null { return 'system-ai-assistant'; case 'system-ai-discovery': case 'discovery': - return 'system-ai-discovery'; + return 'system-ai-assistant'; case 'system-relay': return 'system-relay'; case 'system-billing': diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index ae78d6343a..459d4e3c5f 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -101,11 +101,11 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState - Check model + Fix setup } > @@ -347,15 +347,17 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState 'sm:hidden flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-surface-alt disabled:text-muted rounded-md transition-colors', )} - - - Settings - + + + + Settings + +
diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx index 1e5de33b5e..107830b0b3 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx @@ -1,6 +1,8 @@ import { createMemo, For, Show } from 'solid-js'; import HistoryIcon from 'lucide-solid/icons/history'; import SettingsIcon from 'lucide-solid/icons/settings'; +import ShieldAlertIcon from 'lucide-solid/icons/shield-alert'; +import ListChecksIcon from 'lucide-solid/icons/list-checks'; import { FindingsPanel } from '@/components/AI/FindingsPanel'; import { ApprovalBanner, RunHistoryPanel } from '@/components/patrol'; import { @@ -136,8 +138,7 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt }), ); const hasVisibleWorkGroups = () => - queueIssueCount() > 0 || - workGroupSummaries().some((group) => group.id !== 'stale-protection'); + queueIssueCount() > 0 || workGroupSummaries().some((group) => group.id !== 'stale-protection'); const shouldShowWorkGroups = () => !isHistoryOpen() && !isSetupOnly() && !state.selectedRun() && hasVisibleWorkGroups(); @@ -178,7 +179,7 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt

{workspaceDescription()}

- + 0}>
+ + +
+ } - noun="backup" - segmentKinds={RECOVERABLE_SEGMENT_KINDS} - range={chartRange} - onRangeChange={setChartRange} - timeline={recoverableTimeline} - selectedDateKey={selectedDateKey} - onToggleDay={toggleDay} - metricToggle={{ mode: recoverableMetricMode, onChange: setRecoverableMetricMode }} - /> + > + +
{ }); describe('ProxmoxBackupsTable', () => { - it('defaults to the By date chronological feed with source/location/state columns', async () => { + it('defaults to coverage when protection needs attention and keeps the dated feed one click away', async () => { mockBackupAPIs(); renderInRouter(() => ( @@ -181,10 +181,17 @@ describe('ProxmoxBackupsTable', () => { /> )); - // Default view is the v5-parity backup feed: one row PER backup, so the - // guest with PBS + archive + snapshot artifacts appears on multiple rows, - // sourced and located, not collapsed to a single coverage-posture summary. - expect((await screen.findAllByText('pbs-docker')).length).toBeGreaterThan(1); + await screen.findAllByText('pbs-docker'); + expect(screen.getByRole('button', { name: /coverage/i })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + + await fireEvent.click(screen.getByRole('button', { name: /by date/i })); + + // The chronological feed remains available for forensic review: one row + // per restore point, sourced and located. + expect(screen.getAllByText('pbs-docker').length).toBeGreaterThan(1); expect(screen.getByRole('columnheader', { name: /location/i })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /source/i })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /type/i })).toBeInTheDocument(); @@ -273,6 +280,7 @@ describe('ProxmoxBackupsTable', () => { )); await screen.findAllByText('pbs-docker'); + await fireEvent.click(screen.getByRole('button', { name: /by date/i })); const searchInput = screen.getByPlaceholderText(/search backups by workload/i); await fireEvent.input(searchInput, { target: { value: 'no-such-guest' } }); diff --git a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx index d14a6894c1..8cc517c0e3 100644 --- a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx +++ b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx @@ -839,8 +839,7 @@ const availabilityAddressFor = (machine: Resource): string => { const address = asTrimmedString(availability?.address); if (address) return address; const identityWithIPAddresses = machine.identity as - | (Resource['identity'] & { ipAddresses?: string[] }) - | undefined; + (Resource['identity'] & { ipAddresses?: string[] }) | undefined; const firstIP = asTrimmedString( identityWithIPAddresses?.ipAddresses?.[0] ?? machine.identity?.ips?.[0], ); @@ -1179,6 +1178,10 @@ export const AgentsMachinesTable: Component<{ const columnVisibility = useColumnVisibility( 'pulse:standalone:machines:columns:v3', AGENT_MACHINE_COLUMNS, + [], + undefined, + {}, + ['network', 'diskio', 'uptime', 'temp'], ); const drawer = createPlatformResourceDetailState({ idPrefix: 'agents-machine-drawer' }); const [agentMetadataById, setAgentMetadataById] = createSignal>({}); @@ -1405,7 +1408,7 @@ export const AgentsMachinesTable: Component<{ > @@ -1794,6 +1797,7 @@ export const AgentsMachinesTable: Component<{ open={isExpanded()} detailRowId={detailRowId()} colSpan={detailColspan()} + initialShowHostDetails={false} onClose={() => { drawer.close(machine); }} diff --git a/frontend-modern/src/features/standalone/__tests__/AgentsMachinesTable.test.tsx b/frontend-modern/src/features/standalone/__tests__/AgentsMachinesTable.test.tsx index 8a0a299947..2166bc89bc 100644 --- a/frontend-modern/src/features/standalone/__tests__/AgentsMachinesTable.test.tsx +++ b/frontend-modern/src/features/standalone/__tests__/AgentsMachinesTable.test.tsx @@ -8,10 +8,14 @@ import { RESOURCE_METADATA_CHANGED_EVENT } from '@/utils/resourceMetadataEvents' import { AgentsMachinesTable } from '../AgentsMachinesTable'; vi.mock('@/components/Infrastructure/ResourceDetailDrawer', () => ({ - ResourceDetailDrawer: (props: { initialShowAccessContext?: boolean }) => ( + ResourceDetailDrawer: (props: { + initialShowAccessContext?: boolean; + initialShowHostDetails?: boolean; + }) => (
), })); @@ -200,6 +204,12 @@ describe('AgentsMachinesTable', () => { /> )); + expect(screen.queryByRole('button', { name: 'Sort by Net I/O' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Sort by Disk I/O' })).not.toBeInTheDocument(); + await fireEvent.click(screen.getByTitle('Choose which columns to display')); + await fireEvent.click(screen.getByLabelText('Net I/O')); + await fireEvent.click(screen.getByLabelText('Disk I/O')); + expect(screen.getByRole('button', { name: 'Sort by Net I/O' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Sort by Disk I/O' })).toBeInTheDocument(); expect(screen.getByText('1.00 KB/s')).toBeInTheDocument(); @@ -207,7 +217,6 @@ describe('AgentsMachinesTable', () => { expect(screen.queryByRole('button', { name: 'Sort by IP' })).not.toBeInTheDocument(); expect(screen.getByText('192.168.0.10')).toBeInTheDocument(); - await fireEvent.click(screen.getByTitle('Choose which columns to display')); await fireEvent.click(screen.getByLabelText('IP')); await fireEvent.click(screen.getByLabelText('RAID')); @@ -512,7 +521,10 @@ describe('AgentsMachinesTable', () => { expect( screen.getByRole('button', { name: 'Collapse details for Expandable Machine' }), ).toHaveAttribute('aria-expanded', 'true'); - expect(screen.getByTestId('resource-detail-drawer')).toBeInTheDocument(); + expect(screen.getByTestId('resource-detail-drawer')).toHaveAttribute( + 'data-initial-show-host-details', + 'false', + ); }); it('removes agent machines through row actions with confirmation', async () => { @@ -644,6 +656,8 @@ describe('AgentsMachinesTable', () => { /> )); + await fireEvent.click(screen.getByTitle('Choose which columns to display')); + await fireEvent.click(screen.getByLabelText('Net I/O')); const trigger = container.querySelector('[data-agent-machine-network-trigger="true"]'); expect(trigger).not.toBeNull(); if (!trigger) return; @@ -736,6 +750,8 @@ describe('AgentsMachinesTable', () => { /> )); + await fireEvent.click(screen.getByTitle('Choose which columns to display')); + await fireEvent.click(screen.getByLabelText('Disk I/O')); const trigger = container.querySelector('[data-agent-machine-diskio-trigger="true"]'); expect(trigger).not.toBeNull(); if (!trigger) return; @@ -938,6 +954,8 @@ describe('AgentsMachinesTable', () => { /> )); + await fireEvent.click(screen.getByTitle('Choose which columns to display')); + await fireEvent.click(screen.getByLabelText('Temp')); const trigger = container.querySelector('[data-agent-machine-temperature-trigger="true"]'); expect(trigger).not.toBeNull(); if (!trigger) return; @@ -979,6 +997,8 @@ describe('AgentsMachinesTable', () => { /> )); + await fireEvent.click(screen.getByTitle('Choose which columns to display')); + await fireEvent.click(screen.getByLabelText('Temp')); const pressure = await screen.findByText('Nominal'); expect(pressure).toBeInTheDocument(); diff --git a/frontend-modern/src/features/standalone/agentMachineTableModel.ts b/frontend-modern/src/features/standalone/agentMachineTableModel.ts index 01033dcdb0..f686c64b4e 100644 --- a/frontend-modern/src/features/standalone/agentMachineTableModel.ts +++ b/frontend-modern/src/features/standalone/agentMachineTableModel.ts @@ -54,10 +54,38 @@ export const AGENT_MACHINE_COLUMNS: AgentMachineColumn[] = [ { id: 'cpu', label: 'CPU', kind: 'metric-bar', sortKey: 'cpu' }, { id: 'memory', label: 'Memory', kind: 'metric-bar', sortKey: 'memory' }, { id: 'disk', label: 'Disk', kind: 'metric-bar', sortKey: 'disk' }, - { id: 'network', label: 'Net I/O', kind: 'numeric-value', sortKey: 'network', toggleable: true }, - { id: 'diskio', label: 'Disk I/O', kind: 'numeric-value', sortKey: 'diskio', toggleable: true }, - { id: 'uptime', label: 'Uptime', kind: 'numeric-value', sortKey: 'uptime', toggleable: true }, - { id: 'temp', label: 'Temp', kind: 'numeric-value', sortKey: 'temp', toggleable: true }, + { + id: 'network', + label: 'Net I/O', + kind: 'numeric-value', + sortKey: 'network', + toggleable: true, + defaultHidden: true, + }, + { + id: 'diskio', + label: 'Disk I/O', + kind: 'numeric-value', + sortKey: 'diskio', + toggleable: true, + defaultHidden: true, + }, + { + id: 'uptime', + label: 'Uptime', + kind: 'numeric-value', + sortKey: 'uptime', + toggleable: true, + defaultHidden: true, + }, + { + id: 'temp', + label: 'Temp', + kind: 'numeric-value', + sortKey: 'temp', + toggleable: true, + defaultHidden: true, + }, { id: 'lastSeen', label: 'Last seen', diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 347414dec2..481c2409cd 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -6,6 +6,7 @@ import { useLocation, useNavigate } from '@solidjs/router'; import { logger } from '@/utils/logger'; import { t } from '@/i18n'; import { Card } from '@/components/shared/Card'; +import { Button } from '@/components/shared/Button'; import { PageHeader } from '@/components/shared/PageHeader'; import { notificationStore } from '@/stores/notifications'; @@ -34,6 +35,7 @@ import LayoutDashboard from 'lucide-solid/icons/layout-dashboard'; import History from 'lucide-solid/icons/history'; import Gauge from 'lucide-solid/icons/gauge'; import Send from 'lucide-solid/icons/send'; +import BellOff from 'lucide-solid/icons/bell-off'; import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy'; import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface'; import { OverviewTab } from '@/features/alerts/OverviewTab'; @@ -252,162 +254,205 @@ export function Alerts() { } /> - -
+ +
+
+
+
+
+

Alerts are paused

+

+ Pulse is not evaluating thresholds or sending new alert notifications. Existing + alert history and configuration are preserved. +

+

+ Enable alerts to resume monitoring and unlock thresholds, notifications, and + maintenance schedules. +

+
+
+ +
+ + } + > +
- -
-

{t('alerts.nav.title')}

+
+ +
+

{t('alerts.nav.title')}

+ +
+
+ -
- - - - -
- - {(group) => ( -
- -

- {group.label} -

-
-
- - {(item) => ( - - )} - + +
+ + {(group) => ( +
+ +

+ {group.label} +

+
+
+ + {(item) => ( + + )} + +
-
- )} - + )} + +
-
-
- 0}> -
-
-
- - {(tab) => ( - - )} - +
+ 0}> +
+
+
+ + {(tab) => ( + + )} + +
+
+ +
+ + + + + + + + + + +
- - -
- - - - - - - - - - -
-
- + +
); } diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index 6e488fecc8..8865b07675 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -753,16 +753,14 @@ describe('AIIntelligence entitlement gating', () => { render(() => ); await waitFor(() => { - expect(screen.getByRole('heading', { name: 'Check Patrol model' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Patrol needs setup' })).toBeInTheDocument(); }); expect( screen.getByText( - 'Provider checks can pass while Patrol still needs a tool-call check. Open Provider & Models, then click Check Patrol model.', + 'Patrol cannot check infrastructure until its selected model passes the Patrol tool check.', ), ).toBeInTheDocument(); - expect( - screen.getByText(/Patrol check:\s*Selected model cannot run Patrol tools\./), - ).toBeInTheDocument(); + expect(screen.getByText('Selected model cannot run Patrol tools.')).toBeInTheDocument(); expect( screen.queryByText( 'The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.', @@ -780,13 +778,10 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByText(/Trigger status:/)).not.toBeInTheDocument(); expect(screen.queryByText(/local development safety guard/)).not.toBeInTheDocument(); const providerSettingsLinks = screen.getAllByRole('link', { - name: 'Open Provider & Models', + name: 'Check Patrol model', }); expect(providerSettingsLinks.length).toBeGreaterThan(0); - expect(providerSettingsLinks[0]).toHaveAttribute( - 'href', - '/settings/pulse-intelligence/provider', - ); + expect(providerSettingsLinks[0]).toHaveAttribute('href', '/settings/pulse-intelligence/patrol'); expect(screen.queryByRole('button', { name: /Run Patrol/i })).not.toBeInTheDocument(); expect(triggerPatrolRunMock).not.toHaveBeenCalled(); }); @@ -1849,7 +1844,7 @@ describe('AIIntelligence entitlement gating', () => { render(() => ); await waitFor(() => { - expect(screen.getByRole('heading', { name: 'Check Patrol model' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Patrol needs setup' })).toBeInTheDocument(); }); expect( @@ -1857,21 +1852,19 @@ describe('AIIntelligence entitlement gating', () => { ).not.toBeInTheDocument(); expect( screen.getByText( - 'Provider checks can pass while Patrol still needs a tool-call check. Open Provider & Models, then click Check Patrol model.', + 'Patrol cannot check infrastructure until its selected model passes the Patrol tool check.', ), ).toBeInTheDocument(); - expect( - screen.getByRole('heading', { name: 'Patrol model needs attention' }), - ).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Patrol cannot run yet' })).toBeInTheDocument(); expect( screen.queryByText('Fix the provider connection, then Patrol can check infrastructure.'), ).not.toBeInTheDocument(); - expect(screen.getByText('Patrol check: Provider billing or quota issue')).toBeInTheDocument(); + expect(screen.getByText('Provider billing or quota issue')).toBeInTheDocument(); expect(screen.queryByText('Patrol setup issue')).not.toBeInTheDocument(); expect(screen.queryByText('Patrol readiness issue')).not.toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Open Provider & Models' })).toHaveAttribute( + expect(screen.getByRole('link', { name: 'Check Patrol model' })).toHaveAttribute( 'href', - '/settings/pulse-intelligence/provider', + '/settings/pulse-intelligence/patrol', ); expect(screen.queryByText('Runtime issue')).not.toBeInTheDocument(); expect(screen.queryByText(/regressed \d+×/)).not.toBeInTheDocument(); @@ -1957,8 +1950,8 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.getByRole('heading', { name: 'Open work' })).toBeInTheDocument(); const providerActions = screen.getAllByRole('link', { name: /Check Patrol model/i }); expect(providerActions.length).toBeGreaterThan(0); - expect(providerActions[0]).toHaveAttribute('href', '/settings/pulse-intelligence/provider'); - expect(screen.queryByRole('link', { name: 'Open Provider & Models' })).not.toBeInTheDocument(); + expect(providerActions[0]).toHaveAttribute('href', '/settings/pulse-intelligence/patrol'); + expect(screen.queryByRole('link', { name: 'Open Patrol settings' })).not.toBeInTheDocument(); }); it('does not repeat stale coverage caveats after a successful full patrol verified resources', async () => { @@ -2357,9 +2350,9 @@ describe('AIIntelligence entitlement gating', () => { /Checked 72 resources in 58m\. Patrol ended with a runtime issue: Selected model does not support Patrol tools/i, ), ).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Open Provider & Models' })).toHaveAttribute( + expect(screen.getByRole('link', { name: 'Check Patrol model' })).toHaveAttribute( 'href', - '/settings/pulse-intelligence/provider', + '/settings/pulse-intelligence/patrol', ); expect(findingsPanelState.latestProps).toMatchObject({ filterOverride: 'all', diff --git a/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx b/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx index a845f6a8d0..caa5b5e716 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx +++ b/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx @@ -169,4 +169,20 @@ describe('Alerts read-only presentation', () => { expect(screen.getAllByText('Thresholds').length).toBeGreaterThan(0); expect(navigateSpy).not.toHaveBeenCalledWith('/alerts/overview', { replace: true }); }); + + it('replaces the disabled navigation shell with one clear activation task', async () => { + presentationPolicyIsReadOnlyMock.mockReturnValue(false); + activationStateMock.mockReturnValue('pending_review'); + + render(() => ); + + expect(await screen.findByRole('heading', { name: 'Alerts are paused' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Enable alerts' })).toBeInTheDocument(); + expect( + screen.getByText(/not evaluating thresholds or sending new alert notifications/i), + ).toBeInTheDocument(); + expect(screen.queryByTestId('overview-tab')).not.toBeInTheDocument(); + expect(screen.queryByTestId('alerts-config-surface')).not.toBeInTheDocument(); + expect(screen.queryByText('Thresholds')).not.toBeInTheDocument(); + }); }); diff --git a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts index c71b2eb5d3..51427f16f7 100644 --- a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts @@ -106,8 +106,8 @@ describe('discoveryPresentation', () => { label: 'Settings → API Access', }); expect(getDiscoveryServiceContextSettingsTarget()).toEqual({ - href: '/settings/pulse-intelligence/discovery', - label: 'Open Service Context settings', + href: '/settings/pulse-intelligence/assistant', + label: 'Open Assistant settings', }); expect(getDiscoveryNoConnectedAgentMessage(false)).toBe( 'Commands not enabled. Enable Pulse commands from Settings → Infrastructure for this agent.', diff --git a/frontend-modern/src/utils/__tests__/patrolRunPresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolRunPresentation.test.ts index 6d3cc7a435..1ea11c6864 100644 --- a/frontend-modern/src/utils/__tests__/patrolRunPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolRunPresentation.test.ts @@ -88,8 +88,8 @@ describe('patrolRunPresentation', () => { error_detail: 'Provider rejected tool_choice', }), ).toEqual({ - label: 'Open Provider & Models', - href: '/settings/pulse-intelligence/provider', + label: 'Check Patrol model', + href: '/settings/pulse-intelligence/patrol', }); expect( getPatrolRunPrimaryActionPresentation({ @@ -126,8 +126,8 @@ describe('patrolRunPresentation', () => { }), ).toEqual({ action: { - label: 'Open Provider & Models', - href: '/settings/pulse-intelligence/provider', + label: 'Check Patrol model', + href: '/settings/pulse-intelligence/patrol', }, summary: 'Checked 72 resources in 58m.', outcome: diff --git a/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts b/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts index 269aefbd13..05f875d547 100644 --- a/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts @@ -7,8 +7,8 @@ import { describe('patrolRuntimeActions', () => { it('centralizes the Patrol provider settings action', () => { expect(PATROL_PROVIDER_SETTINGS_ACTION).toEqual({ - label: 'Open Provider & Models', - href: '/settings/pulse-intelligence/provider', + label: 'Check Patrol model', + href: '/settings/pulse-intelligence/patrol', }); const action = getPatrolProviderSettingsAction(); diff --git a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts index 7e7f1bf77d..09f46ee1f5 100644 --- a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts @@ -372,8 +372,8 @@ describe('getPatrolSummaryPresentation', () => { ] as never, }), ).toEqual({ - label: 'Open Provider & Models', - href: '/settings/pulse-intelligence/provider', + label: 'Check Patrol model', + href: '/settings/pulse-intelligence/patrol', }); }); @@ -745,8 +745,7 @@ describe('getPatrolSummaryPresentation', () => { }), ).toEqual({ title: 'Needs full check', - description: - 'Recent targeted checks covered 1 resource. Run Patrol to check everything.', + description: 'Recent targeted checks covered 1 resource. Run Patrol to check everything.', compactLabel: 'Partial check', tone: 'warning', }); @@ -789,8 +788,7 @@ describe('getPatrolSummaryPresentation', () => { }), ).toEqual({ title: 'Needs full check', - description: - 'Recent follow-up checks covered 1 resource. Run Patrol to check everything.', + description: 'Recent follow-up checks covered 1 resource. Run Patrol to check everything.', compactLabel: 'Partial check', tone: 'warning', }); diff --git a/frontend-modern/src/utils/discoveryPresentation.ts b/frontend-modern/src/utils/discoveryPresentation.ts index 8446aea215..e0fc309485 100644 --- a/frontend-modern/src/utils/discoveryPresentation.ts +++ b/frontend-modern/src/utils/discoveryPresentation.ts @@ -7,7 +7,7 @@ import { getInfrastructureSettingsLocationLabel, getInfrastructureSettingsTarget, } from '@/utils/infrastructureSettingsPresentation'; -import { SETTINGS_PULSE_INTELLIGENCE_DISCOVERY_PATH } from '@/routing/resourceLinks'; +import { SETTINGS_PULSE_INTELLIGENCE_ASSISTANT_PATH } from '@/routing/resourceLinks'; export interface DiscoveryIdentifiedSummary { serviceName: string; @@ -322,8 +322,8 @@ export function getDiscoveryApiAccessSettingsTarget() { export function getDiscoveryServiceContextSettingsTarget() { return { - href: SETTINGS_PULSE_INTELLIGENCE_DISCOVERY_PATH, - label: 'Open Service Context settings', + href: SETTINGS_PULSE_INTELLIGENCE_ASSISTANT_PATH, + label: 'Open Assistant settings', } as const; } diff --git a/frontend-modern/src/utils/patrolRuntimeActions.ts b/frontend-modern/src/utils/patrolRuntimeActions.ts index 5ce577da72..c791e4bbcc 100644 --- a/frontend-modern/src/utils/patrolRuntimeActions.ts +++ b/frontend-modern/src/utils/patrolRuntimeActions.ts @@ -1,4 +1,4 @@ -import { SETTINGS_PROVIDER_MODELS_PATH } from '@/components/Settings/settingsNavigationModel'; +import { settingsTabPath } from '@/components/Settings/settingsNavigationModel'; export interface PatrolRuntimeActionPresentation { label: string; @@ -6,8 +6,8 @@ export interface PatrolRuntimeActionPresentation { } export const PATROL_PROVIDER_SETTINGS_ACTION: PatrolRuntimeActionPresentation = { - label: 'Open Provider & Models', - href: SETTINGS_PROVIDER_MODELS_PATH, + label: 'Check Patrol model', + href: settingsTabPath('system-ai-patrol'), }; export const getPatrolProviderSettingsAction = (): PatrolRuntimeActionPresentation => ({