diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 16a566f436..e37bd0ddcd 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -307,7 +307,11 @@ work extends shared components instead of creating new local variants. `frontend-modern/src/components/Settings/useAISettingsState.ts`, and `frontend-modern/src/utils/aiSettingsPresentation.ts` must present that surface to operators as `Assistant & Patrol` plus provider/model - configuration rather than as a generic `AI Services` shell. + configuration rather than as a generic `AI Services` shell. Runtime + controls inside `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` + must likewise describe discovery as workload discovery that supplies + concrete service context to Pulse Assistant and Patrol, not as a generic + AI context feature. ## Forbidden Paths @@ -1925,8 +1929,9 @@ settings work must extend those section owners instead of re-inlining large runtime subsections into the shell. That same AI settings boundary now also owns `frontend-modern/src/utils/aiSettingsPresentation.ts`, so shared loading, -empty, OAuth, and action/error copy for the settings shell stays on one -governed helper instead of drifting back into section-local strings. +empty, OAuth, action/error, shell-description, and workload-discovery copy +for the settings shell stays on one governed helper instead of drifting back +into section-local strings. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`, `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`, `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`, diff --git a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx index 591b0fd872..14057f5ee3 100644 --- a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx +++ b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx @@ -10,6 +10,10 @@ import { getAIControlLevelDescription, getAIControlLevelPanelClass, } from '@/utils/aiControlLevelPresentation'; +import { + getAISettingsWorkloadDiscoveryHelpContent, + getAISettingsWorkloadDiscoverySummary, +} from '@/utils/aiSettingsPresentation'; import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { UPGRADE_ACTION_LABEL, @@ -41,7 +45,7 @@ export const AIRuntimeControlsSection: Component d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> - Discovery Settings + Workload Discovery {state.form.discoveryIntervalHours > 0 @@ -68,13 +72,9 @@ export const AIRuntimeControlsSection: Component
@@ -107,15 +107,14 @@ export const AIRuntimeControlsSection: Component

{state.form.discoveryIntervalHours === 0 - ? 'Discovery runs only when you click "Update Discovery" on a resource' - : 'Discovery will automatically re-scan resources at this interval'} + ? 'Workload discovery runs only when you click "Update Discovery" on a resource' + : 'Workload discovery will automatically re-scan resources at this interval'}

- Discovery gives Pulse AI workload context, so responses can reference concrete - services and commands instead of generic advice. + {getAISettingsWorkloadDiscoverySummary().text}

diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index 11c7c21121..2adcb1842f 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -10,6 +10,8 @@ import SettingsPanel from '@/components/shared/SettingsPanel'; import { Toggle } from '@/components/shared/Toggle'; import { PATROL_PATH } from '@/routing/resourceLinks'; import { + AI_SETTINGS_PANEL_DESCRIPTION, + AI_SETTINGS_PANEL_TITLE, getAISettingsLoadingState, getAISettingsLoadErrorMessage, getAISettingsRetryLabel, @@ -22,8 +24,8 @@ export const AISettings: Component = () => { return ( <> { expect(aiRuntimeControlsSectionSource).toContain('@/utils/upgradePresentation'); expect(aiRuntimeControlsSectionSource).toContain('UPGRADE_ACTION_LABEL'); expect(aiRuntimeControlsSectionSource).toContain('UPGRADE_TRIAL_LABEL'); - expect(aiRuntimeControlsSectionSource).toContain('Discovery Settings'); + expect(aiRuntimeControlsSectionSource).toContain('Workload Discovery'); + expect(aiRuntimeControlsSectionSource).toContain('getAISettingsWorkloadDiscoveryHelpContent'); + expect(aiRuntimeControlsSectionSource).toContain('getAISettingsWorkloadDiscoverySummary'); expect(aiRuntimeControlsSectionSource).toContain('Pulse Permission Level'); expect(aiRuntimeControlsSectionSource).toContain('destination={state.upgradeAutofixDestination()}'); expect(aiRuntimeControlsSectionSource).not.toContain('href={state.upgradeAutofixDestination().href}'); @@ -1519,6 +1521,8 @@ describe('Settings architecture guardrails', () => { const allowedTitleExpression = tab === 'system-updates' ? `title={UPDATES_PANEL_COPY.title}` + : tab === 'system-ai' + ? 'title={AI_SETTINGS_PANEL_TITLE}' : tab === 'system-billing' ? 'title={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle}' : `title="${title}"`; diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index a37afb00dd..fc6b819698 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -1,6 +1,10 @@ import type { SettingsHeaderMetaMap } from './settingsNavigationModel'; import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation'; import { RELAY_SETTINGS_DESCRIPTION } from '@/utils/relayPresentation'; +import { + AI_SETTINGS_PANEL_DESCRIPTION, + AI_SETTINGS_PANEL_TITLE, +} from '@/utils/aiSettingsPresentation'; export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { proxmox: { @@ -30,8 +34,8 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { description: 'Manage backup/snapshot polling plus configuration export and import workflows.', }, 'system-ai': { - title: 'Assistant & Patrol', - description: 'Configure providers, models, Pulse Assistant, and Patrol.', + title: AI_SETTINGS_PANEL_TITLE, + description: AI_SETTINGS_PANEL_DESCRIPTION, }, 'system-relay': { title: 'Remote Access', diff --git a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts index 143fc0fad8..98842b38d0 100644 --- a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + AI_SETTINGS_PANEL_DESCRIPTION, + AI_SETTINGS_PANEL_TITLE, getAICredentialsClearErrorMessage, getAIOAuthErrorMessage, getAIChatSessionsEmptyState, @@ -16,9 +18,26 @@ import { getAISettingsRetryLabel, getAISettingsSaveErrorMessage, getAISettingsToggleErrorMessage, + getAISettingsWorkloadDiscoveryHelpContent, + getAISettingsWorkloadDiscoverySummary, } from '@/utils/aiSettingsPresentation'; describe('aiSettingsPresentation', () => { + it('returns the canonical assistant and patrol shell framing copy', () => { + expect(AI_SETTINGS_PANEL_TITLE).toBe('Assistant & Patrol'); + expect(AI_SETTINGS_PANEL_DESCRIPTION).toBe( + 'Configure providers and models for Pulse Assistant and Patrol.', + ); + expect(getAISettingsWorkloadDiscoveryHelpContent()).toEqual({ + title: 'What is workload discovery?', + description: + 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.', + }); + expect(getAISettingsWorkloadDiscoverySummary()).toEqual({ + text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.', + }); + }); + it('returns the canonical provider-backed ready presentation', () => { expect( getAISettingsReadinessPresentation({ diff --git a/frontend-modern/src/utils/aiSettingsPresentation.ts b/frontend-modern/src/utils/aiSettingsPresentation.ts index bcfd1d0d1c..386fdc851c 100644 --- a/frontend-modern/src/utils/aiSettingsPresentation.ts +++ b/frontend-modern/src/utils/aiSettingsPresentation.ts @@ -21,6 +21,9 @@ const AI_OAUTH_ERROR_MESSAGES: Record = { save_failed: 'Unable to save OAuth credentials.', }; +export const AI_SETTINGS_PANEL_TITLE = 'Assistant & Patrol'; +export const AI_SETTINGS_PANEL_DESCRIPTION = + 'Configure providers and models for Pulse Assistant and Patrol.'; export const AI_SETTINGS_LOAD_MODELS_ERROR = 'Unable to load models.'; export const AI_SETTINGS_LOAD_CHAT_SESSIONS_ERROR = 'Unable to load chat sessions.'; export const AI_SETTINGS_LOAD_FAILURE_MESSAGE = @@ -31,6 +34,20 @@ export function getAIProviderTestResultTextClass(success: boolean): string { return success ? 'text-green-600' : 'text-red-600'; } +export function getAISettingsWorkloadDiscoveryHelpContent() { + return { + title: 'What is workload discovery?', + description: + 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.', + } as const; +} + +export function getAISettingsWorkloadDiscoverySummary() { + return { + text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.', + } as const; +} + export function getAISettingsReadinessPresentation( input: AISettingsReadinessInput, ): AISettingsReadinessPresentation { diff --git a/tests/integration/tests/15-settings-shell-consistency.spec.ts b/tests/integration/tests/15-settings-shell-consistency.spec.ts index a451731444..d2dbb710af 100644 --- a/tests/integration/tests/15-settings-shell-consistency.spec.ts +++ b/tests/integration/tests/15-settings-shell-consistency.spec.ts @@ -38,8 +38,8 @@ const SETTINGS_SHELL_ROUTES = [ }, { route: '/settings/system-ai', - title: 'AI Settings', - description: 'Configure AI providers, model defaults, Pulse Assistant, and Patrol automation.', + title: 'Assistant & Patrol', + description: 'Configure providers and models for Pulse Assistant and Patrol.', }, { route: '/settings/system-updates', diff --git a/tests/integration/tests/52-ai-settings-provider-setup.spec.ts b/tests/integration/tests/52-ai-settings-provider-setup.spec.ts index 1df7b568a1..e67bdbe812 100644 --- a/tests/integration/tests/52-ai-settings-provider-setup.spec.ts +++ b/tests/integration/tests/52-ai-settings-provider-setup.spec.ts @@ -42,7 +42,7 @@ const baseSettings = (): MockAISettings => ({ available_models: [], }); -test.describe("AI settings provider setup", () => { +test.describe("Assistant & Patrol settings provider setup", () => { test("OpenRouter setup submits credentials without a hardcoded model and renders backend-selected state", async ({ page, }, testInfo) => { @@ -108,6 +108,9 @@ test.describe("AI settings provider setup", () => { await page.goto("/settings/system-ai", { waitUntil: "domcontentloaded" }); await expect(page.getByRole("heading", { name: "Assistant & Patrol", level: 1 })).toBeVisible(); + await expect( + page.getByText("Configure providers and models for Pulse Assistant and Patrol.", { exact: true }), + ).toBeVisible(); await page.getByRole("button", { name: /enable assistant and patrol/i }).click(); const setupDialog = page.getByRole("dialog", { name: "Set up Pulse Assistant" }); @@ -125,7 +128,17 @@ test.describe("AI settings provider setup", () => { expect(updateRequests[0]).not.toHaveProperty("model"); await expect(page.getByText("Advanced Model Selection")).toBeVisible(); - await expect(page.getByRole("button", { name: /enable ai services/i })).toHaveAttribute( + const workloadDiscoveryToggle = page.getByRole("button", { name: /workload discovery/i }); + await expect(workloadDiscoveryToggle).toBeVisible(); + await workloadDiscoveryToggle.click(); + await expect(page.getByText("Enable workload discovery")).toBeVisible(); + await expect( + page.getByText( + "Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.", + { exact: true }, + ), + ).toBeVisible(); + await expect(page.getByRole("button", { name: /enable assistant and patrol/i })).toHaveAttribute( "aria-pressed", "true", );