diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index bd1455422..c9ef4c7ae 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -26,28 +26,31 @@ work extends shared components instead of creating new local variants. 4. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts` 5. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` 6. `frontend-modern/src/components/Settings/AISettings.tsx` -7. `frontend-modern/src/components/Settings/AuditLogPanel.tsx` -8. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx` -9. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` -10. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx` -11. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx` -12. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` -13. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` -14. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` -15. `frontend-modern/src/components/Settings/SettingsPageShell.tsx` -16. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts` -17. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx` -18. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` -19. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts` -20. `tests/integration/tests/15-settings-shell-consistency.spec.ts` -21. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts` -22. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` -23. `frontend-modern/src/features/` -24. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` -25. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` -26. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx` -27. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx` -28. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx` +7. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx` +8. `frontend-modern/src/components/Settings/AISettingsDialogs.tsx` +9. `frontend-modern/src/components/Settings/aiSettingsModel.ts` +10. `frontend-modern/src/components/Settings/AuditLogPanel.tsx` +11. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx` +12. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` +13. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx` +14. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx` +15. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` +16. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` +17. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` +18. `frontend-modern/src/components/Settings/SettingsPageShell.tsx` +19. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts` +20. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx` +21. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` +22. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts` +23. `tests/integration/tests/15-settings-shell-consistency.spec.ts` +24. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts` +25. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` +26. `frontend-modern/src/features/` +27. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` +28. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` +29. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx` +30. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx` +31. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx` ## Shared Boundaries @@ -256,6 +259,12 @@ well: `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` and representative top-level panels such as `frontend-modern/src/components/Settings/APIAccessPanel.tsx`, `frontend-modern/src/components/Settings/AISettings.tsx`, +`frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`, +`frontend-modern/src/components/Settings/AISettingsDialogs.tsx`, and +`frontend-modern/src/components/Settings/aiSettingsModel.ts` now also define +the canonical AI settings runtime boundary: `AISettings.tsx` is the shell and +controller, while provider configuration, modal flows, and provider metadata +must extend those extracted owners instead of being re-inlined into the shell. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`, `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`, `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`, diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 3ecdf0343..8b619b813 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -1579,7 +1579,10 @@ "frontend-modern/src/components/shared/" ], "owned_files": [ + "frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx", "frontend-modern/src/components/Settings/AISettings.tsx", + "frontend-modern/src/components/Settings/AISettingsDialogs.tsx", + "frontend-modern/src/components/Settings/aiSettingsModel.ts", "frontend-modern/src/components/Settings/APIAccessPanel.tsx", "frontend-modern/src/components/Settings/AuditLogPanel.tsx", "frontend-modern/src/components/Settings/AuditWebhookPanel.tsx", @@ -1631,7 +1634,10 @@ "label": "settings shell framing proof", "match_prefixes": [], "match_files": [ + "frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx", "frontend-modern/src/components/Settings/AISettings.tsx", + "frontend-modern/src/components/Settings/AISettingsDialogs.tsx", + "frontend-modern/src/components/Settings/aiSettingsModel.ts", "frontend-modern/src/components/Settings/APIAccessPanel.tsx", "frontend-modern/src/components/Settings/AuditLogPanel.tsx", "frontend-modern/src/components/Settings/AuditWebhookPanel.tsx", diff --git a/frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx b/frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx new file mode 100644 index 000000000..fd36bf2b2 --- /dev/null +++ b/frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx @@ -0,0 +1,264 @@ +import { For, Show, type Accessor, type Component, type Setter } from 'solid-js'; +import type { SetStoreFunction } from 'solid-js/store'; +import { HelpIcon } from '@/components/shared/HelpIcon'; +import { controlClass } from '@/components/shared/Form'; +import { getAIProviderHealthPresentation } from '@/utils/aiProviderHealthPresentation'; +import { getAIProviderDisplayName } from '@/utils/aiProviderPresentation'; +import { getAIProviderTestResultTextClass } from '@/utils/aiSettingsPresentation'; +import type { AIProvider, AISettings as AISettingsType } from '@/types/ai'; +import { + AI_PROVIDER_CONFIGS, + AI_PROVIDERS, + type AIProviderCredentialsFormState, + type ProviderHealthState, + type ProviderTestResult, + isAIProviderConfigured, +} from '@/components/Settings/aiSettingsModel'; + +export interface AIProviderConfigurationSectionProps { + settings: Accessor; + form: AIProviderCredentialsFormState; + setForm: SetStoreFunction; + expandedProviders: Accessor>; + setExpandedProviders: Setter>; + providerHealth: Record; + preflightRunning: Accessor; + preflightLastCheckedAt: Accessor; + providerIssueCount: Accessor; + testingProvider: Accessor; + providerTestResult: Accessor; + saving: Accessor; + runProviderPreflight: () => Promise; + handleTestProvider: (provider: AIProvider) => Promise; + handleClearProvider: (provider: AIProvider) => Promise; +} + +export const AIProviderConfigurationSection: Component = ( + props, +) => { + const toggleProvider = (provider: AIProvider) => { + const next = new Set(props.expandedProviders()); + if (next.has(provider)) { + next.delete(provider); + } else { + next.add(provider); + } + props.setExpandedProviders(next); + }; + + const isConfigured = (provider: AIProvider) => isAIProviderConfigured(provider, props.settings()); + + const providerIssueProviders = () => + AI_PROVIDERS.filter((provider) => props.providerHealth[provider].status === 'error'); + + return ( +
+
+
+

+ + + + Provider Configuration +

+ +
+

+ Configure API keys for each provider you want to use. Models from all configured providers + will appear in the model selectors. +

+ +

+ Last checked: {new Date(props.preflightLastCheckedAt()!).toLocaleTimeString()} +

+
+ 0}> +
+

+ {props.providerIssueCount()} provider{props.providerIssueCount() === 1 ? '' : 's'}{' '} + configured but currently not usable. +

+ + {(provider) => ( +

+ {getAIProviderDisplayName(provider) || provider}:{' '} + {props.providerHealth[provider].message} +

+ )} +
+
+
+
+ +
+ + {(config) => { + const configured = () => isConfigured(config.provider); + const expanded = () => props.expandedProviders().has(config.provider); + const health = () => props.providerHealth[config.provider]; + const testResult = () => + props.providerTestResult()?.provider === config.provider + ? props.providerTestResult() + : null; + + return ( +
+ + +
+ + {(extraField) => ( +
+ + + props.setForm(extraField().inputField, event.currentTarget.value) + } + placeholder={extraField().placeholder} + class={controlClass()} + disabled={props.saving()} + /> +
+ )} +
+ + + + + + props.setForm(config.inputField, event.currentTarget.value) + } + placeholder={ + configured() && config.configuredPlaceholder + ? config.configuredPlaceholder + : config.placeholder + } + class={controlClass()} + disabled={props.saving()} + /> + + +

{config.helperText}

+
+ +
+

+ + {config.actionLinkLabel} + + + {config.actionLinkSuffix} + +

+ +
+ + +
+
+
+ + +

+ {testResult()?.message} +

+
+
+
+
+ ); + }} +
+
+
+ ); +}; + +export default AIProviderConfigurationSection; diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index b27ae8e92..422544b8c 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -1,12 +1,18 @@ import { Component, Show, createSignal, onMount, For, createMemo, createEffect } from 'solid-js'; import { createStore } from 'solid-js/store'; import { useNavigate } from '@solidjs/router'; -import SettingsPanel from '@/components/shared/SettingsPanel'; -import { Dialog } from '@/components/shared/Dialog'; +import { AIProviderConfigurationSection } from '@/components/Settings/AIProviderConfigurationSection'; +import { AISettingsDialogs } from '@/components/Settings/AISettingsDialogs'; import { - SelectionCardGroup, - type SelectionCardOption, -} from '@/components/shared/SelectionCardGroup'; + AI_PROVIDERS, + createInitialProviderHealth, + groupModelsByProvider, + isAIProviderConfigured, + isModelProviderConfigured, + type ProviderHealthState, + type ProviderTestResult, +} from '@/components/Settings/aiSettingsModel'; +import SettingsPanel from '@/components/shared/SettingsPanel'; import { Toggle } from '@/components/shared/Toggle'; import { HelpIcon } from '@/components/shared/HelpIcon'; import { formField, labelClass, controlClass } from '@/components/shared/Form'; @@ -15,10 +21,6 @@ import { aiChatStore } from '@/stores/aiChat'; import { logger } from '@/utils/logger'; import { AIAPI } from '@/api/ai'; import { AIChatAPI, type ChatSession, type FileChange } from '@/api/aiChat'; -import { - getAIProviderHealthPresentation, - type AIProviderHealthStatus, -} from '@/utils/aiProviderHealthPresentation'; import { getAIControlLevelBadgeClass, getAIControlLevelDescription, @@ -27,14 +29,12 @@ import { type AIControlLevel, } from '@/utils/aiControlLevelPresentation'; import { getAIProviderDisplayName, getProviderFromModelId } from '@/utils/aiProviderPresentation'; -import { getAISessionDiffStatusPresentation } from '@/utils/aiSessionDiffPresentation'; import { getAICredentialsClearErrorMessage, getAIOAuthErrorMessage, getAIChatSessionsEmptyState, getAIChatSessionsLoadErrorMessage, getAIChatSessionsLoadingState, - getAIProviderTestResultTextClass, getAISessionDiffErrorMessage, getAISessionRevertErrorMessage, getAISessionSummarizeErrorMessage, @@ -62,83 +62,6 @@ import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics' import { showSuccess, showWarning } from '@/utils/toast'; import type { AISettings as AISettingsType, AIProvider, AuthMethod } from '@/types/ai'; -// Providers are now configured via accordion sections, not a single-provider selector - -const AI_PROVIDERS: AIProvider[] = [ - 'anthropic', - 'openai', - 'openrouter', - 'deepseek', - 'gemini', - 'ollama', -]; - -const AI_SETUP_PROVIDER_OPTIONS: SelectionCardOption[] = [ - { value: 'anthropic', title: 'Anthropic', description: 'Claude' }, - { value: 'openai', title: 'OpenAI', description: 'ChatGPT' }, - { value: 'openrouter', title: 'OpenRouter', description: 'Gateway' }, - { value: 'deepseek', title: 'DeepSeek', description: 'V3' }, - { value: 'gemini', title: 'Gemini', description: 'Google' }, - { value: 'ollama', title: 'Ollama', description: 'Local' }, -]; - -type ProviderHealthState = { - status: AIProviderHealthStatus; - message: string; -}; - -const createInitialProviderHealth = (): Record => ({ - anthropic: { status: 'not_configured', message: '' }, - openai: { status: 'not_configured', message: '' }, - openrouter: { status: 'not_configured', message: '' }, - deepseek: { status: 'not_configured', message: '' }, - gemini: { status: 'not_configured', message: '' }, - ollama: { status: 'not_configured', message: '' }, -}); - -// Check if a provider is configured based on settings -function isProviderConfigured(provider: string, settings: AISettingsType | null): boolean { - if (!settings) return false; - switch (provider) { - case 'anthropic': - return settings.anthropic_configured; - case 'openai': - return settings.openai_configured; - case 'openrouter': - return settings.openrouter_configured; - case 'deepseek': - return settings.deepseek_configured; - case 'gemini': - return settings.gemini_configured; - case 'ollama': - return settings.ollama_configured; - default: - return false; - } -} - -// Check if a model's provider is configured -function isModelProviderConfigured(modelId: string, settings: AISettingsType | null): boolean { - const provider = getProviderFromModelId(modelId); - return isProviderConfigured(provider, settings); -} - -// Group models by provider for optgroup rendering -function groupModelsByProvider( - models: { id: string; name: string; description?: string }[], -): Map { - const grouped = new Map(); - - for (const model of models) { - const provider = getProviderFromModelId(model.id); - const existing = grouped.get(provider) || []; - existing.push(model); - grouped.set(provider, existing); - } - - return grouped; -} - export const AISettings: Component = () => { const navigate = useNavigate(); const [settings, setSettings] = createSignal(null); @@ -171,12 +94,8 @@ export const AISettings: Component = () => { ); // Per-provider test state - const [testingProvider, setTestingProvider] = createSignal(null); - const [providerTestResult, setProviderTestResult] = createSignal<{ - provider: string; - success: boolean; - message: string; - } | null>(null); + const [testingProvider, setTestingProvider] = createSignal(null); + const [providerTestResult, setProviderTestResult] = createSignal(null); const [providerHealth, setProviderHealth] = createStore>( createInitialProviderHealth(), ); @@ -232,9 +151,7 @@ export const AISettings: Component = () => { // First-time setup modal state const [showSetupModal, setShowSetupModal] = createSignal(false); - const [setupProvider, setSetupProvider] = createSignal< - 'anthropic' | 'openai' | 'openrouter' | 'deepseek' | 'gemini' | 'ollama' - >('anthropic'); + const [setupProvider, setSetupProvider] = createSignal('anthropic'); const [setupApiKey, setSetupApiKey] = createSignal(''); const [setupOllamaUrl, setSetupOllamaUrl] = createSignal('http://localhost:11434'); const [setupSaving, setSetupSaving] = createSignal(false); @@ -329,7 +246,7 @@ export const AISettings: Component = () => { ? String(data.cost_budget_usd_30d) : '', requestTimeoutSeconds: data.request_timeout_seconds ?? 300, - controlLevel: normalizeAIControlLevel(data.control_level), + controlLevel: normalizeAIControlLevel(data.control_level), protectedGuests: Array.isArray(data.protected_guests) ? data.protected_guests.join(', ') : '', discoveryEnabled: data.discovery_enabled ?? false, discoveryIntervalHours: data.discovery_interval_hours ?? 0, @@ -408,6 +325,78 @@ export const AISettings: Component = () => { const formatDiffStats = (change: FileChange) => `+${change.added} -${change.removed}`; + const handleCloseSetupModal = () => { + setShowSetupModal(false); + setSetupApiKey(''); + }; + + const handleSetupSubmit = async () => { + setSetupSaving(true); + try { + const payload: Record = { enabled: true }; + + if (setupProvider() === 'anthropic') { + if (!setupApiKey().trim()) { + notificationStore.error('Please enter your Anthropic API key'); + return; + } + payload.anthropic_api_key = setupApiKey().trim(); + payload.model = 'anthropic:claude-sonnet-4-20250514'; + } else if (setupProvider() === 'openai') { + if (!setupApiKey().trim()) { + notificationStore.error('Please enter your OpenAI API key'); + return; + } + payload.openai_api_key = setupApiKey().trim(); + payload.model = 'openai:gpt-4o'; + } else if (setupProvider() === 'openrouter') { + if (!setupApiKey().trim()) { + notificationStore.error('Please enter your OpenRouter API key'); + return; + } + payload.openrouter_api_key = setupApiKey().trim(); + payload.model = 'openrouter:openai/gpt-4o-mini'; + } else if (setupProvider() === 'deepseek') { + if (!setupApiKey().trim()) { + notificationStore.error('Please enter your DeepSeek API key'); + return; + } + payload.deepseek_api_key = setupApiKey().trim(); + payload.model = 'deepseek:deepseek-chat'; + } else if (setupProvider() === 'gemini') { + if (!setupApiKey().trim()) { + notificationStore.error('Please enter your Google Gemini API key'); + return; + } + payload.gemini_api_key = setupApiKey().trim(); + payload.model = 'gemini:gemini-2.5-flash'; + } else { + if (!setupOllamaUrl().trim()) { + notificationStore.error('Please enter your Ollama server URL'); + return; + } + payload.ollama_base_url = setupOllamaUrl().trim(); + payload.model = 'ollama:llama3.2:latest'; + } + + const updated = await AIAPI.updateSettings(payload); + setSettings(updated); + setForm('enabled', true); + resetForm(updated); + void runProviderPreflight(updated); + handleCloseSetupModal(); + notificationStore.success('Pulse Assistant enabled! You can customize settings below.'); + loadModels(); + aiChatStore.notifySettingsChanged(); + } catch (error) { + logger.error('[AISettings] Setup failed:', error); + const message = error instanceof Error ? error.message : 'Setup failed'; + notificationStore.error(message); + } finally { + setSetupSaving(false); + } + }; + const handleSessionSummarize = async () => { const sessionId = selectedSessionId(); if (!sessionId) { @@ -510,15 +499,20 @@ export const AISettings: Component = () => { const checkProviderHealth = async ( provider: AIProvider, opts: { notify?: boolean; storeManualResult?: boolean } = {}, - ): Promise<{ success: boolean; message: string; provider: string }> => { + ): Promise => { try { const result = await AIAPI.testProvider(provider); + const normalizedResult: ProviderTestResult = { + provider, + success: result.success, + message: result.message, + }; setProviderHealth(provider, { status: result.success ? 'ok' : 'error', message: result.message || '', }); if (opts.storeManualResult) { - setProviderTestResult(result); + setProviderTestResult(normalizedResult); } if (opts.notify) { if (result.success) { @@ -527,10 +521,10 @@ export const AISettings: Component = () => { notificationStore.error(`${provider}: ${result.message}`); } } - return result; + return normalizedResult; } catch (error) { const message = error instanceof Error ? error.message : 'Connection test failed'; - const result = { provider, success: false, message }; + const result: ProviderTestResult = { provider, success: false, message }; setProviderHealth(provider, { status: 'error', message, @@ -552,7 +546,7 @@ export const AISettings: Component = () => { } const configuredProviders = AI_PROVIDERS.filter((provider) => - isProviderConfigured(provider, current), + isAIProviderConfigured(provider, current), ); for (const provider of AI_PROVIDERS) { if (!configuredProviders.includes(provider)) { @@ -616,7 +610,7 @@ export const AISettings: Component = () => { const selectedModel = form.model.trim(); if (selectedModel && form.enabled) { const modelProvider = getProviderFromModelId(selectedModel); - if (!isProviderConfigured(modelProvider, settings())) { + if (!isAIProviderConfigured(modelProvider, settings())) { // Check if any API key is being added in this save for this provider const isAddingCredential = (modelProvider === 'anthropic' && form.anthropicApiKey.trim()) || @@ -786,12 +780,11 @@ export const AISettings: Component = () => { } }; - const handleTestProvider = async (provider: string) => { - const typedProvider = provider as AIProvider; - setTestingProvider(typedProvider); + const handleTestProvider = async (provider: AIProvider) => { + setTestingProvider(provider); setProviderTestResult(null); try { - await checkProviderHealth(typedProvider, { notify: true, storeManualResult: true }); + await checkProviderHealth(provider, { notify: true, storeManualResult: true }); } catch (error) { logger.error(`[AISettings] Test ${provider} failed:`, error); const message = error instanceof Error ? error.message : 'Connection test failed'; @@ -802,7 +795,7 @@ export const AISettings: Component = () => { } }; - const handleClearProvider = async (provider: string) => { + const handleClearProvider = async (provider: AIProvider) => { // Check if this is the last configured provider const s = settings(); const configuredCount = [ @@ -813,7 +806,7 @@ export const AISettings: Component = () => { s?.gemini_configured, s?.ollama_configured, ].filter(Boolean).length; - const isLastProvider = configuredCount === 1 && isProviderConfigured(provider, s); + const isLastProvider = configuredCount === 1 && isAIProviderConfigured(provider, s); // Check if current model uses this provider const currentModel = form.model.trim(); @@ -862,7 +855,9 @@ export const AISettings: Component = () => { aiChatStore.notifySettingsChanged(); } catch (error) { logger.error(`[AISettings] Clear ${provider} failed:`, error); - const message = getAICredentialsClearErrorMessage(error instanceof Error ? error.message : ''); + const message = getAICredentialsClearErrorMessage( + error instanceof Error ? error.message : '', + ); notificationStore.error(message); } finally { setSaving(false); @@ -1076,7 +1071,7 @@ export const AISettings: Component = () => { {/* Show configured providers first */} isProviderConfigured(p, settings()), + ([p]) => isAIProviderConfigured(p, settings()), )} > {([provider, models]) => ( @@ -1094,7 +1089,7 @@ export const AISettings: Component = () => { {/* Show unconfigured providers in a separate section with warning */} !isProviderConfigured(p, settings()), + ([p]) => !isAIProviderConfigured(p, settings()), )} > {([provider, models]) => ( @@ -1310,699 +1305,24 @@ export const AISettings: Component = () => { - {/* Provider Configuration - Configure API keys for all providers */} -
-
-
-

- - - - Provider Configuration -

- -
-

- Configure API keys for each provider you want to use. Models from all configured - providers will appear in the model selectors. -

- -

- Last checked: {new Date(preflightLastCheckedAt()!).toLocaleTimeString()} -

-
- 0}> -
-

- {providerIssueCount()} provider{providerIssueCount() === 1 ? '' : 's'}{' '} - configured but currently not usable. -

- providerHealth[provider].status === 'error', - )} - > - {(provider) => ( -

- - {getAIProviderDisplayName(provider) || provider}: - {' '} - {providerHealth[provider].message} -

- )} -
-
-
-
- - {/* Provider Accordions */} -
- {/* Anthropic */} -
- - -
- setForm('anthropicApiKey', e.currentTarget.value)} - placeholder={ - settings()?.anthropic_configured - ? '••••••••••• (configured)' - : 'sk-ant-...' - } - class={controlClass()} - disabled={saving()} - /> -
-

- - Get API key → - -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* OpenAI */} -
- - -
- setForm('openaiApiKey', e.currentTarget.value)} - placeholder={ - settings()?.openai_configured ? '••••••••••• (configured)' : 'sk-...' - } - class={controlClass()} - disabled={saving()} - /> -
- - setForm('openaiBaseUrl', e.currentTarget.value)} - placeholder="https://api.together.xyz/v1 (optional)" - class={controlClass()} - disabled={saving()} - /> -
-
-

- - Get API key → - -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* OpenRouter */} -
- - -
- setForm('openrouterApiKey', e.currentTarget.value)} - placeholder={ - settings()?.openrouter_configured - ? '••••••••••• (configured)' - : 'sk-or-...' - } - class={controlClass()} - disabled={saving()} - /> -

- Uses https://openrouter.ai/api/v1 automatically. -

-
-

- - Get API key → - -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* DeepSeek */} -
- - -
- setForm('deepseekApiKey', e.currentTarget.value)} - placeholder={ - settings()?.deepseek_configured ? '••••••••••• (configured)' : 'sk-...' - } - class={controlClass()} - disabled={saving()} - /> -
-

- - Get API key → - -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* Google Gemini */} -
- - -
- setForm('geminiApiKey', e.currentTarget.value)} - placeholder={ - settings()?.gemini_configured ? '••••••••••• (configured)' : 'AIza...' - } - class={controlClass()} - disabled={saving()} - /> -
-

- - Get API key → - -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* Ollama */} -
- - -
-
- - setForm('ollamaBaseUrl', e.currentTarget.value)} - placeholder="http://localhost:11434" - class={controlClass()} - disabled={saving()} - /> -
-
-

- - Learn about Ollama → - - · Free & local -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
-
+
+ runProviderPreflight()} + handleTestProvider={handleTestProvider} + handleClearProvider={handleClearProvider} + />
{/* Discovery Settings - Collapsible */} @@ -2502,263 +1822,24 @@ export const AISettings: Component = () => { - {/* Session Diff Modal */} - - setShowDiffModal(false)} - panelClass="max-w-2xl" - ariaLabel="Session file changes" - > -
-
-
-

Session File Changes

-

{diffSessionLabel() || 'Selected session'}

-
- -
-
- -
-

Summary

-

{diffSummary()}

-
-
-
- - {(file) => { - const diffStatus = getAISessionDiffStatusPresentation(file.status); - return ( -
-
- - {diffStatus.label} - - {file.path} -
- {formatDiffStats(file)} -
- ); - }} -
-
-
-
-
-
- - {/* First-time Setup Modal */} - - { - setShowSetupModal(false); - setSetupApiKey(''); - }} - panelClass="max-w-md" - closeOnBackdrop={false} - ariaLabel="Set up Pulse Assistant" - > -
- {/* Header */} -
-

Set Up Pulse Assistant

-

Choose a provider to get started

-
- - {/* Provider Selection */} -
- - - {/* API Key / URL Input */} - - - setSetupApiKey(e.currentTarget.value)} - placeholder={ - setupProvider() === 'anthropic' - ? 'sk-ant-...' - : setupProvider() === 'gemini' - ? 'AIza...' - : setupProvider() === 'openrouter' - ? 'sk-or-...' - : 'sk-...' - } - class="w-full px-3 py-2 border border-border rounded-md bg-surface focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

- - Get your API key → - -

-
- } - > -
- - setSetupOllamaUrl(e.currentTarget.value)} - placeholder="http://localhost:11434" - class="w-full px-3 py-2 border border-border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

- Ollama runs locally - no API key needed -

-
- -
- - {/* Footer */} -
- - -
-
- - + ); }; diff --git a/frontend-modern/src/components/Settings/AISettingsDialogs.tsx b/frontend-modern/src/components/Settings/AISettingsDialogs.tsx new file mode 100644 index 000000000..99ce29baf --- /dev/null +++ b/frontend-modern/src/components/Settings/AISettingsDialogs.tsx @@ -0,0 +1,190 @@ +import { For, Show, type Accessor, type Component, type Setter } from 'solid-js'; +import { Dialog } from '@/components/shared/Dialog'; +import { SelectionCardGroup } from '@/components/shared/SelectionCardGroup'; +import { getAISessionDiffStatusPresentation } from '@/utils/aiSessionDiffPresentation'; +import type { FileChange } from '@/api/aiChat'; +import type { AIProvider } from '@/types/ai'; +import { + AI_SETUP_PROVIDER_OPTIONS, + getAIProviderConfig, +} from '@/components/Settings/aiSettingsModel'; + +export interface AISettingsDialogsProps { + showDiffModal: Accessor; + setShowDiffModal: Setter; + diffFiles: Accessor; + diffSummary: Accessor; + diffSessionLabel: Accessor; + formatDiffStats: (change: FileChange) => string; + showSetupModal: Accessor; + setupProvider: Accessor; + setSetupProvider: Setter; + setupApiKey: Accessor; + setSetupApiKey: Setter; + setupOllamaUrl: Accessor; + setSetupOllamaUrl: Setter; + setupSaving: Accessor; + handleCloseSetupModal: () => void; + handleSetupSubmit: () => Promise; +} + +export const AISettingsDialogs: Component = (props) => { + const setupProviderConfig = () => getAIProviderConfig(props.setupProvider()); + + return ( + <> + + props.setShowDiffModal(false)} + panelClass="max-w-2xl" + ariaLabel="Session file changes" + > +
+
+
+

Session File Changes

+

{props.diffSessionLabel() || 'Selected session'}

+
+ +
+
+ +
+

Summary

+

{props.diffSummary()}

+
+
+
+ + {(file) => { + const diffStatus = getAISessionDiffStatusPresentation(file.status); + return ( +
+
+ + {diffStatus.label} + + {file.path} +
+ + {props.formatDiffStats(file)} + +
+ ); + }} +
+
+
+
+
+
+ + + +
+
+

Set Up Pulse Assistant

+

Choose a provider to get started

+
+ +
+ + + + + props.setSetupApiKey(event.currentTarget.value)} + placeholder={setupProviderConfig().placeholder} + class="w-full px-3 py-2 border border-border rounded-md bg-surface focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

+ + Get your API key → + +

+
+ } + > +
+ + props.setSetupOllamaUrl(event.currentTarget.value)} + placeholder="http://localhost:11434" + class="w-full px-3 py-2 border border-border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

+ Ollama runs locally - no API key needed +

+
+ +
+ +
+ + +
+ +
+
+ + ); +}; + +export default AISettingsDialogs; diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index 1d256cd47..e8ec66974 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -26,7 +26,10 @@ import alertThresholdsPresentationSource from '@/utils/alertThresholdsPresentati import alertThresholdsSectionPresentationSource from '@/utils/alertThresholdsSectionPresentation.ts?raw'; import loginSource from '@/components/Login.tsx?raw'; import settingsSource from '../Settings.tsx?raw'; -import aiSettingsSource from '../AISettings.tsx?raw'; +import aiSettingsShellSource from '../AISettings.tsx?raw'; +import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw'; +import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw'; +import aiSettingsModelSource from '../aiSettingsModel.ts?raw'; import reportingPanelSource from '../ReportingPanel.tsx?raw'; import updatesSettingsPanelSource from '../UpdatesSettingsPanel.tsx?raw'; import suggestProfileModalSource from '../SuggestProfileModal.tsx?raw'; @@ -78,6 +81,13 @@ import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.t import problemResourcesTableSource from '@/pages/DashboardPanels/ProblemResourcesTable.tsx?raw'; import workloadTypeBadgesSource from '@/components/shared/workloadTypeBadges.ts?raw'; import workloadTypePresentationSource from '@/utils/workloadTypePresentation.ts?raw'; + +const aiSettingsSource = [ + aiSettingsShellSource, + aiProviderConfigurationSectionSource, + aiSettingsDialogsSource, + aiSettingsModelSource, +].join('\n'); import dashboardGuestPresentationSource from '@/utils/dashboardGuestPresentation.ts?raw'; import containerUpdatesSource from '@/stores/containerUpdates.ts?raw'; import websocketStoreSource from '@/stores/websocket.ts?raw'; @@ -127,9 +137,7 @@ describe('monitored-system model guardrails', () => { expect(agentProfilesPanelSource).not.toContain( 'No agents connected. Install an agent to assign profiles.', ); - expect(agentProfilesPresentationSource).toContain( - 'export function getAgentProfilesEmptyState', - ); + expect(agentProfilesPresentationSource).toContain('export function getAgentProfilesEmptyState'); expect(agentProfilesPresentationSource).toContain( 'export function getAgentProfileAssignmentsEmptyState', ); @@ -162,7 +170,9 @@ describe('monitored-system model guardrails', () => { "const hostResources = createMemo(() => byType('host'))", ); expect(apiTokenManagerSource).not.toContain('isAppContainerDiscoveryResourceType'); - expect(apiTokenManagerSource).not.toContain("notificationStore.error('Failed to load API tokens')"); + expect(apiTokenManagerSource).not.toContain( + "notificationStore.error('Failed to load API tokens')", + ); expect(apiTokenManagerSource).not.toContain( "notificationStore.error('Failed to generate API token')", ); @@ -176,7 +186,9 @@ describe('monitored-system model guardrails', () => { expect(infrastructureOperationsStateSource).not.toContain('previousHostTypes'); expect(infrastructureOperationsStateSource).not.toContain('const allHosts = createMemo('); expect(infrastructureOperationsStateSource).toContain('@/utils/unifiedAgentStatusPresentation'); - expect(infrastructureOperationsStateSource).not.toContain('const MONITORING_STOPPED_STATUS_LABEL ='); + expect(infrastructureOperationsStateSource).not.toContain( + 'const MONITORING_STOPPED_STATUS_LABEL =', + ); expect(infrastructureOperationsStateSource).not.toContain('const ALLOW_RECONNECT_LABEL ='); expect(infrastructureOperationsStateSource).toContain('withPrivilegeEscalation'); expect(infrastructureOperationsStateSource).toContain('@/utils/agentCapabilityPresentation'); @@ -195,7 +207,9 @@ describe('monitored-system model guardrails', () => { expect(infrastructureOperationsStateSource).toContain('@/utils/unifiedAgentStatusPresentation'); expect(infrastructureOperationsStateSource).not.toContain('const statusBadgeClass ='); expect(infrastructureOperationsStateSource).not.toContain('const statusBadgeClasses ='); - expect(infrastructureOperationsStateSource).toContain('getUnifiedAgentLookupStatusPresentation'); + expect(infrastructureOperationsStateSource).toContain( + 'getUnifiedAgentLookupStatusPresentation', + ); expect(unifiedAgentStatusPresentationSource).toContain( 'export function getUnifiedAgentStatusPresentation', ); @@ -338,34 +352,20 @@ describe('monitored-system model guardrails', () => { expect(aiSettingsPresentationSource).toContain( 'export function getAISettingsReadinessPresentation', ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAISettingsLoadingState', - ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAISettingsLoadErrorMessage', - ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAISettingsRetryLabel', - ); + expect(aiSettingsPresentationSource).toContain('export function getAISettingsLoadingState'); + expect(aiSettingsPresentationSource).toContain('export function getAISettingsLoadErrorMessage'); + expect(aiSettingsPresentationSource).toContain('export function getAISettingsRetryLabel'); expect(aiSettingsSource).toContain('SelectionCardGroup'); expect(aiSettingsSource).toContain('variant="compact"'); expect(updatesSettingsPanelSource).toContain('SelectionCardGroup'); expect(updatesSettingsPanelSource).toContain('variant="detail"'); - expect(aiSettingsPresentationSource).toContain( - 'export function getAIChatSessionsLoadingState', - ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAIChatSessionsEmptyState', - ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAIModelsLoadErrorMessage', - ); + expect(aiSettingsPresentationSource).toContain('export function getAIChatSessionsLoadingState'); + expect(aiSettingsPresentationSource).toContain('export function getAIChatSessionsEmptyState'); + expect(aiSettingsPresentationSource).toContain('export function getAIModelsLoadErrorMessage'); expect(aiSettingsPresentationSource).toContain( 'export function getAIChatSessionsLoadErrorMessage', ); - expect(aiSettingsPresentationSource).toContain( - 'export function getAIOAuthErrorMessage', - ); + expect(aiSettingsPresentationSource).toContain('export function getAIOAuthErrorMessage'); expect(suggestProfileModalSource).toContain('@/utils/agentProfileSuggestionPresentation'); expect(suggestProfileModalSource).toContain('AGENT_PROFILE_SUGGESTION_EXAMPLE_PROMPTS'); expect(suggestProfileModalSource).toContain('getAgentProfileSuggestionLoadingState'); @@ -494,7 +494,9 @@ describe('monitored-system model guardrails', () => { expect(organizationOverviewPanelSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationAccessPanelSource).toContain('ORGANIZATION_MEMBER_ROLE_OPTIONS'); expect(organizationAccessPanelSource).not.toContain("{ value: 'viewer', label: 'Viewer' }"); - expect(organizationAccessPanelSource).not.toContain("notificationStore.error('User ID is required')"); + expect(organizationAccessPanelSource).not.toContain( + "notificationStore.error('User ID is required')", + ); expect(organizationAccessPanelSource).not.toContain( "notificationStore.error(error instanceof Error ? error.message : 'Failed to add member')", ); @@ -512,7 +514,7 @@ describe('monitored-system model guardrails', () => { expect(organizationSharingPanelSource).toContain('ORGANIZATION_SHARE_ROLE_OPTIONS'); expect(organizationSharingPanelSource).toContain('normalizeOrganizationShareRole'); expect(organizationSharingPanelSource).not.toContain( - "const accessRoleOptions: Array<{ value: ShareAccessRole; label: string }> = [", + 'const accessRoleOptions: Array<{ value: ShareAccessRole; label: string }> = [', ); expect(organizationSharingPanelSource).not.toContain('const normalizeShareRole ='); expect(organizationSharingPanelSource).not.toContain( @@ -701,15 +703,11 @@ describe('monitored-system model guardrails', () => { ); expect(resourcePickerSource).not.toContain('No resources match your filters'); expect(reportableResourceTypesSource).toContain('export const REPORTABLE_RESOURCE_TYPES ='); - expect(reportableResourceTypesSource).toContain( - 'export const RESOURCE_PICKER_TYPE_FILTERS', - ); + expect(reportableResourceTypesSource).toContain('export const RESOURCE_PICKER_TYPE_FILTERS'); expect(reportableResourceTypesSource).toContain( 'export const getResourcePickerTypeFilterLabel', ); - expect(reportableResourceTypesSource).toContain( - 'export const getResourcePickerEmptyState', - ); + expect(reportableResourceTypesSource).toContain('export const getResourcePickerEmptyState'); expect(reportableResourceTypesSource).toContain( 'export const matchesReportableResourceTypeFilter =', ); @@ -758,9 +756,7 @@ describe('monitored-system model guardrails', () => { expect(systemSettingsPresentationSource).toContain( 'export function getBackupIntervalSelectValue', ); - expect(systemSettingsPresentationSource).toContain( - 'export function getBackupIntervalSummary', - ); + expect(systemSettingsPresentationSource).toContain('export function getBackupIntervalSummary'); expect(systemSettingsPresentationSource).toContain( 'export function getSystemSettingsSaveErrorMessage', ); @@ -787,15 +783,25 @@ describe('monitored-system model guardrails', () => { expect(ssoProvidersPanelSource).toContain('SSOProviderTypeIcon'); expect(ssoProvidersPanelSource).not.toContain('No SSO providers configured'); expect(ssoProvidersPanelSource).not.toContain('Loading SSO providers...'); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to load SSO providers')"); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to load SSO providers')", + ); expect(ssoProvidersPanelSource).not.toContain( "notificationStore.error('Failed to load provider details')", ); expect(ssoProvidersPanelSource).not.toContain("notificationStore.success('Provider deleted')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to delete provider')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.success('Connection test successful')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to test connection')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Please enter an IdP Metadata URL')"); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to delete provider')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.success('Connection test successful')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to test connection')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Please enter an IdP Metadata URL')", + ); expect(ssoProvidersPanelSource).not.toContain('provider.type.toUpperCase()'); expect(ssoProvidersPanelSource).not.toContain("provider.type === 'oidc' ? ("); expect(ssoProvidersPanelSource).not.toContain( @@ -807,9 +813,7 @@ describe('monitored-system model guardrails', () => { expect(ssoProviderPresentationSource).toContain('export function getSSOProviderTypeLabel'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderSummary'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderCardClass'); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOProviderAddButtonLabel', - ); + expect(ssoProviderPresentationSource).toContain('export function getSSOProviderAddButtonLabel'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderModalTitle'); expect(ssoProviderPresentationSource).toContain( 'export function getSSOProviderEmptyStateTitle', @@ -817,12 +821,8 @@ describe('monitored-system model guardrails', () => { expect(ssoProviderPresentationSource).toContain( 'export function getSSOProviderEmptyStateDescription', ); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOProvidersLoadingState', - ); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOTestResultPresentation', - ); + expect(ssoProviderPresentationSource).toContain('export function getSSOProvidersLoadingState'); + expect(ssoProviderPresentationSource).toContain('export function getSSOTestResultPresentation'); expect(ssoProviderPresentationSource).toContain( 'export function getSSOCertificatePresentation', ); @@ -854,8 +854,12 @@ describe('monitored-system model guardrails', () => { expect(rolesPanelSource).toContain('createDefaultRBACPermission'); expect(rolesPanelSource).toContain('getRBACFeatureGateCopy'); expect(rolesPanelSource).toContain('getRolesEmptyState'); - expect(rolesPanelSource).not.toContain("const ACTIONS = ['read', 'write', 'delete', 'admin', '*']"); - expect(rolesPanelSource).not.toContain("const RESOURCES = ['settings', 'audit_logs', 'nodes', 'users', 'license', '*']"); + expect(rolesPanelSource).not.toContain( + "const ACTIONS = ['read', 'write', 'delete', 'admin', '*']", + ); + expect(rolesPanelSource).not.toContain( + "const RESOURCES = ['settings', 'audit_logs', 'nodes', 'users', 'license', '*']", + ); expect(rolesPanelSource).not.toContain('Custom Roles (Pro)'); expect(rolesPanelSource).not.toContain('No roles available.'); expect(rbacPermissionsSource).toContain('export const RBAC_PERMISSION_ACTIONS'); @@ -865,12 +869,8 @@ describe('monitored-system model guardrails', () => { expect(rbacPresentationSource).toContain('export function getRolesEmptyState'); expect(rbacPresentationSource).toContain('export function getRolesLoadErrorMessage'); expect(rbacPresentationSource).toContain('export function getRolesSaveErrorMessage'); - expect(rbacPresentationSource).toContain( - 'export function getUserAssignmentsLoadErrorMessage', - ); - expect(rbacPresentationSource).toContain( - 'export function getUserAssignmentsEmptyStateCopy', - ); + expect(rbacPresentationSource).toContain('export function getUserAssignmentsLoadErrorMessage'); + expect(rbacPresentationSource).toContain('export function getUserAssignmentsEmptyStateCopy'); expect(userAssignmentsPanelSource).toContain('@/utils/rbacPresentation'); expect(userAssignmentsPanelSource).toContain('getRBACFeatureGateCopy'); expect(userAssignmentsPanelSource).toContain('getUserAssignmentsEmptyStateCopy'); @@ -924,9 +924,7 @@ describe('monitored-system model guardrails', () => { expect(thresholdsTableSource).toContain('CONTAINERS_FILTER_EMPTY_STATE'); expect(thresholdsTableSource).toContain('getAlertThresholdsGuestFilterPresentation'); expect(thresholdsTableSource).toContain('getAlertThresholdsBackupOrphanedPresentation'); - expect(thresholdsTableSource).toContain( - 'getAlertThresholdsDockerIgnoredPrefixesPresentation', - ); + expect(thresholdsTableSource).toContain('getAlertThresholdsDockerIgnoredPrefixesPresentation'); expect(thresholdsTableSource).not.toContain('No PBS servers configured.'); expect(thresholdsTableSource).not.toContain('No VMs or containers found.'); expect(thresholdsTableSource).not.toContain('No nodes match the current filters.'); @@ -994,9 +992,7 @@ describe('monitored-system model guardrails', () => { expect(alertThresholdsPresentationSource).toContain( 'export const NODE_THRESHOLDS_FILTER_EMPTY_STATE', ); - expect(alertThresholdsPresentationSource).toContain( - 'export const PMG_THRESHOLDS_EMPTY_STATE', - ); + expect(alertThresholdsPresentationSource).toContain('export const PMG_THRESHOLDS_EMPTY_STATE'); expect(alertThresholdsPresentationSource).toContain( 'export const ALERT_THRESHOLDS_SEARCH_PLACEHOLDER', ); @@ -1120,7 +1116,9 @@ describe('monitored-system model guardrails', () => { expect(settingsSource).not.toContain( "stateHosts={(state.resources ?? []).filter((r) => r.type === 'host')}", ); - expect(infrastructureSettingsStateSource).toContain('@/utils/infrastructureSettingsPresentation'); + expect(infrastructureSettingsStateSource).toContain( + '@/utils/infrastructureSettingsPresentation', + ); expect(infrastructureSettingsStateSource).toContain('getDiscoveryScanStartErrorMessage'); expect(infrastructureSettingsStateSource).toContain('getDiscoverySettingUpdateErrorMessage'); expect(infrastructureSettingsStateSource).toContain('getDiscoverySubnetUpdateErrorMessage'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 70fb54ecc..06ef805fc 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -13,6 +13,9 @@ import auditWebhookPanelSource from '../AuditWebhookPanel.tsx?raw'; import billingAdminPanelSource from '../BillingAdminPanel.tsx?raw'; import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw'; import aiSettingsPanelSource from '../AISettings.tsx?raw'; +import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw'; +import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw'; +import aiSettingsModelSource from '../aiSettingsModel.ts?raw'; import networkSettingsPanelSource from '../NetworkSettingsPanel.tsx?raw'; import updatesSettingsPanelSource from '../UpdatesSettingsPanel.tsx?raw'; import recoverySettingsPanelSource from '../RecoverySettingsPanel.tsx?raw'; @@ -45,6 +48,9 @@ const extractedModules = [ '../InfrastructureWorkspace.tsx', '../InfrastructureInstallPanel.tsx', '../InfrastructureReportingPanel.tsx', + '../AIProviderConfigurationSection.tsx', + '../AISettingsDialogs.tsx', + '../aiSettingsModel.ts', '../ProxmoxSettingsPanel.tsx', '../SettingsDialogs.tsx', '../SettingsPageShell.tsx', @@ -222,8 +228,8 @@ describe('Settings architecture guardrails', () => { expect(commercialBillingModelSource).toContain('buildSelfHostedCommercialPlanModel'); expect(commercialBillingModelSource).toContain('buildHostedCommercialPlanModel'); expect(commercialBillingModelSource).toContain('buildHostedCommercialUsageModel'); - expect(proLicensePanelSource).toContain("./CommercialBillingSections"); - expect(proLicensePanelSource).toContain("buildSelfHostedCommercialPlanModel"); + expect(proLicensePanelSource).toContain('./CommercialBillingSections'); + expect(proLicensePanelSource).toContain('buildSelfHostedCommercialPlanModel'); expect(proLicensePanelSource).toContain('SelfHostedCommercialActivationSection'); expect(proLicensePanelSource).toContain('MonitoredSystemLedgerPanel'); expect(proLicensePanelSource).toContain('CommercialBillingShell'); @@ -231,9 +237,9 @@ describe('Settings architecture guardrails', () => { expect(proLicensePanelSource).toContain('CommercialStatGrid'); expect(selfHostedCommercialActivationSectionSource).toContain('License / Activation Key'); expect(selfHostedCommercialActivationSectionSource).toContain('Start 14-day Pro Trial'); - expect(organizationBillingPanelSource).toContain("./CommercialBillingSections"); - expect(organizationBillingPanelSource).toContain("buildHostedCommercialPlanModel"); - expect(organizationBillingPanelSource).toContain("buildHostedCommercialUsageModel"); + expect(organizationBillingPanelSource).toContain('./CommercialBillingSections'); + expect(organizationBillingPanelSource).toContain('buildHostedCommercialPlanModel'); + expect(organizationBillingPanelSource).toContain('buildHostedCommercialUsageModel'); expect(organizationBillingPanelSource).toContain('CommercialBillingShell'); expect(organizationBillingPanelSource).toContain('CommercialSection'); expect(organizationBillingPanelSource).toContain('CommercialUsageMeters'); @@ -284,13 +290,15 @@ describe('Settings architecture guardrails', () => { expect(infrastructureInstallPanelSource).toContain('useInfrastructureOperationsState'); expect(infrastructureReportingPanelSource).toContain('useInfrastructureOperationsState'); expect(infrastructureOperationsControllerSource).toContain('useInfrastructureOperationsState'); - expect(infrastructureOperationsStateSource).toContain('export const useInfrastructureOperationsState'); + expect(infrastructureOperationsStateSource).toContain( + 'export const useInfrastructureOperationsState', + ); expect(infrastructureInstallPanelSource).not.toContain(' { - expect(infrastructureOperationsStateSource).toContain("./infrastructureOperationsModel"); + expect(infrastructureOperationsStateSource).toContain('./infrastructureOperationsModel'); expect(infrastructureOperationsStateSource).not.toContain('const INSTALL_PROFILE_OPTIONS'); expect(infrastructureOperationsStateSource).not.toContain('const buildCommandsByPlatform ='); expect(infrastructureOperationsStateSource).not.toContain( @@ -303,6 +311,16 @@ describe('Settings architecture guardrails', () => { expect(infrastructureOperationsModelSource).toContain('export const buildCommandsByPlatform'); }); + it('keeps AI settings sub-surfaces behind extracted runtime owners', () => { + expect(aiSettingsPanelSource).toContain('@/components/Settings/AIProviderConfigurationSection'); + expect(aiSettingsPanelSource).toContain('@/components/Settings/AISettingsDialogs'); + expect(aiSettingsPanelSource).toContain('@/components/Settings/aiSettingsModel'); + expect(aiProviderConfigurationSectionSource).toContain('@/components/Settings/aiSettingsModel'); + expect(aiSettingsDialogsSource).toContain('@/components/Settings/aiSettingsModel'); + expect(aiSettingsModelSource).toContain('export const AI_PROVIDER_CONFIGS'); + expect(aiSettingsModelSource).toContain('export const AI_SETUP_PROVIDER_OPTIONS'); + }); + it('routes every top-level settings surface through the canonical panel shell framing', () => { for (const [panelName, source] of Object.entries(topLevelSettingsPanelSources)) { const usesCanonicalShell = diff --git a/frontend-modern/src/components/Settings/aiSettingsModel.ts b/frontend-modern/src/components/Settings/aiSettingsModel.ts new file mode 100644 index 000000000..2078e15a0 --- /dev/null +++ b/frontend-modern/src/components/Settings/aiSettingsModel.ts @@ -0,0 +1,213 @@ +import type { SelectionCardOption } from '@/components/shared/SelectionCardGroup'; +import type { AIProviderHealthStatus } from '@/utils/aiProviderHealthPresentation'; +import { getProviderFromModelId } from '@/utils/aiProviderPresentation'; +import type { AIProvider, AISettings as AISettingsType } from '@/types/ai'; + +export type AIProviderCredentialsFormState = { + anthropicApiKey: string; + openaiApiKey: string; + openrouterApiKey: string; + deepseekApiKey: string; + geminiApiKey: string; + ollamaBaseUrl: string; + openaiBaseUrl: string; +}; + +export type AIProviderConfig = { + provider: AIProvider; + title: string; + configuredLabel?: string; + inputType: 'password' | 'url'; + inputField: keyof AIProviderCredentialsFormState; + placeholder: string; + configuredPlaceholder?: string; + actionLinkLabel: string; + actionLinkHref: string; + actionLinkSuffix?: string; + helperText?: string; + extraField?: { + label: string; + helpContentId?: string; + inputField: 'openaiBaseUrl'; + placeholder: string; + }; + clearTitle: string; +}; + +export type ProviderHealthState = { + status: AIProviderHealthStatus; + message: string; +}; + +export type ProviderTestResult = { + provider: AIProvider; + success: boolean; + message: string; +}; + +export type AIAvailableModel = { + id: string; + name: string; + description?: string; +}; + +export const AI_PROVIDERS: AIProvider[] = [ + 'anthropic', + 'openai', + 'openrouter', + 'deepseek', + 'gemini', + 'ollama', +]; + +export const AI_SETUP_PROVIDER_OPTIONS: SelectionCardOption[] = [ + { value: 'anthropic', title: 'Anthropic', description: 'Claude' }, + { value: 'openai', title: 'OpenAI', description: 'ChatGPT' }, + { value: 'openrouter', title: 'OpenRouter', description: 'Gateway' }, + { value: 'deepseek', title: 'DeepSeek', description: 'V3' }, + { value: 'gemini', title: 'Gemini', description: 'Google' }, + { value: 'ollama', title: 'Ollama', description: 'Local' }, +]; + +export const AI_PROVIDER_CONFIGS: AIProviderConfig[] = [ + { + provider: 'anthropic', + title: 'Anthropic', + configuredLabel: 'Configured', + inputType: 'password', + inputField: 'anthropicApiKey', + placeholder: 'sk-ant-...', + configuredPlaceholder: '••••••••••• (configured)', + actionLinkLabel: 'Get API key →', + actionLinkHref: 'https://console.anthropic.com/settings/keys', + clearTitle: 'Clear API key', + }, + { + provider: 'openai', + title: 'OpenAI', + configuredLabel: 'Configured', + inputType: 'password', + inputField: 'openaiApiKey', + placeholder: 'sk-...', + configuredPlaceholder: '••••••••••• (configured)', + actionLinkLabel: 'Get API key →', + actionLinkHref: 'https://platform.openai.com/api-keys', + clearTitle: 'Clear API key', + extraField: { + label: 'Custom Base URL', + helpContentId: 'ai.openai.baseUrl', + inputField: 'openaiBaseUrl', + placeholder: 'https://api.together.xyz/v1 (optional)', + }, + }, + { + provider: 'openrouter', + title: 'OpenRouter', + configuredLabel: 'Configured', + inputType: 'password', + inputField: 'openrouterApiKey', + placeholder: 'sk-or-...', + configuredPlaceholder: '••••••••••• (configured)', + actionLinkLabel: 'Get API key →', + actionLinkHref: 'https://openrouter.ai/keys', + helperText: 'Uses https://openrouter.ai/api/v1 automatically.', + clearTitle: 'Clear API key', + }, + { + provider: 'deepseek', + title: 'DeepSeek', + configuredLabel: 'Configured', + inputType: 'password', + inputField: 'deepseekApiKey', + placeholder: 'sk-...', + configuredPlaceholder: '••••••••••• (configured)', + actionLinkLabel: 'Get API key →', + actionLinkHref: 'https://platform.deepseek.com/api_keys', + clearTitle: 'Clear API key', + }, + { + provider: 'gemini', + title: 'Google Gemini', + configuredLabel: 'Configured', + inputType: 'password', + inputField: 'geminiApiKey', + placeholder: 'AIza...', + configuredPlaceholder: '••••••••••• (configured)', + actionLinkLabel: 'Get API key →', + actionLinkHref: 'https://aistudio.google.com/app/apikey', + clearTitle: 'Clear API key', + }, + { + provider: 'ollama', + title: 'Ollama', + configuredLabel: 'Available', + inputType: 'url', + inputField: 'ollamaBaseUrl', + placeholder: 'http://localhost:11434', + actionLinkLabel: 'Learn about Ollama →', + actionLinkHref: 'https://ollama.ai', + actionLinkSuffix: ' · Free & local', + clearTitle: 'Clear Ollama URL', + }, +]; + +export const createInitialProviderHealth = (): Record => ({ + anthropic: { status: 'not_configured', message: '' }, + openai: { status: 'not_configured', message: '' }, + openrouter: { status: 'not_configured', message: '' }, + deepseek: { status: 'not_configured', message: '' }, + gemini: { status: 'not_configured', message: '' }, + ollama: { status: 'not_configured', message: '' }, +}); + +export function getAIProviderConfig(provider: AIProvider): AIProviderConfig { + const config = AI_PROVIDER_CONFIGS.find((entry) => entry.provider === provider); + if (!config) { + throw new Error(`Unknown AI provider: ${provider}`); + } + return config; +} + +export function isAIProviderConfigured( + provider: AIProvider | string, + settings: AISettingsType | null, +): boolean { + if (!settings) return false; + switch (provider) { + case 'anthropic': + return settings.anthropic_configured; + case 'openai': + return settings.openai_configured; + case 'openrouter': + return settings.openrouter_configured; + case 'deepseek': + return settings.deepseek_configured; + case 'gemini': + return settings.gemini_configured; + case 'ollama': + return settings.ollama_configured; + default: + return false; + } +} + +export function isModelProviderConfigured( + modelId: string, + settings: AISettingsType | null, +): boolean { + const provider = getProviderFromModelId(modelId); + return isAIProviderConfigured(provider, settings); +} + +export function groupModelsByProvider(models: AIAvailableModel[]): Map { + const grouped = new Map(); + + for (const model of models) { + const provider = getProviderFromModelId(model.id); + const existing = grouped.get(provider) || []; + existing.push(model); + grouped.set(provider, existing); + } + + return grouped; +} diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index aa7a21214..d877d99d1 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -286,7 +286,10 @@ import runHistoryPanelSource from '@/components/patrol/RunHistoryPanel.tsx?raw'; import runToolCallTraceSource from '@/components/patrol/RunToolCallTrace.tsx?raw'; import diagnosticsPanelSource from '@/components/Settings/DiagnosticsPanel.tsx?raw'; import diagnosticsPresentationSource from '@/utils/diagnosticsPresentation.ts?raw'; -import aiSettingsSource from '@/components/Settings/AISettings.tsx?raw'; +import aiSettingsShellSource from '@/components/Settings/AISettings.tsx?raw'; +import aiProviderConfigurationSectionSource from '@/components/Settings/AIProviderConfigurationSection.tsx?raw'; +import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw'; +import aiSettingsModelSource from '@/components/Settings/aiSettingsModel.ts?raw'; import aiIntelligenceSource from '@/pages/AIIntelligence.tsx?raw'; import patrolIntelligenceSurfaceSource from '@/features/patrol/PatrolIntelligenceSurface.tsx?raw'; import aiQuickstartPresentationSource from '@/utils/aiQuickstartPresentation.ts?raw'; @@ -306,6 +309,13 @@ import remediationPresentationSource from '@/utils/remediationPresentation.ts?ra import aiChatPresentationSource from '@/utils/aiChatPresentation.ts?raw'; import infrastructureDetailsDrawerSource from '@/components/shared/InfrastructureDetailsDrawer.tsx?raw'; +const aiSettingsSource = [ + aiSettingsShellSource, + aiProviderConfigurationSectionSource, + aiSettingsDialogsSource, + aiSettingsModelSource, +].join('\n'); + describe('frontend resource type boundaries', () => { it('keeps the shared compatibility adapter narrow and explicit', () => { expect(resourceTypeCompatSource).toContain('export const canonicalizeFrontendResourceType'); @@ -409,15 +419,17 @@ describe('frontend resource type boundaries', () => { expect(recoverySummarySource).toContain('buildRecoveryPostureSegments'); expect(recoverySummarySource).toContain('RECOVERY_SUMMARY_TIME_RANGES'); expect(recoverySummarySource).toContain('buildRecoveryFreshnessBuckets'); - expect(recoverySummarySource).not.toContain("const RECOVERY_TIME_RANGES: readonly string[] = ['7d', '30d', '90d']"); - expect(recoverySummarySource).not.toContain('const RECOVERY_TIME_RANGE_LABELS: Record'); - expect(recoverySummarySource).not.toContain("const FRESHNESS_LABELS:"); + expect(recoverySummarySource).not.toContain( + "const RECOVERY_TIME_RANGES: readonly string[] = ['7d', '30d', '90d']", + ); + expect(recoverySummarySource).not.toContain( + 'const RECOVERY_TIME_RANGE_LABELS: Record', + ); + expect(recoverySummarySource).not.toContain('const FRESHNESS_LABELS:'); expect(recoverySource).toContain('getRecoveryArtifactModePresentation'); expect(recoverySource).not.toContain('const MODE_LABELS: Record'); expect(recoverySource).not.toContain('const MODE_BADGE_CLASS: Record'); - expect(recoverySource).not.toContain( - 'const CHART_SEGMENT_CLASS: Record', - ); + expect(recoverySource).not.toContain('const CHART_SEGMENT_CLASS: Record'); expect(recoverySource).toContain('getRecoveryIssueRailClass'); expect(recoverySource).not.toContain( "const ISSUE_RAIL_CLASS: Record, string>", @@ -536,10 +548,16 @@ describe('frontend resource type boundaries', () => { expect(recoverySource).not.toContain( 'inline-flex items-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content hover:bg-surface-hover', ); - expect(recoverySource).not.toContain('rounded-md p-1 hover:text-base-content hover:bg-surface-hover'); - expect(recoverySource).not.toContain("const labelEvery = dayCount <= 7 ? 1 : dayCount <= 30 ? 3 : 10"); - expect(recoverySource).not.toContain("class=\"flex items-center gap-1\""); - expect(recoverySource).not.toContain("class=\"inline-flex rounded border border-border bg-surface p-0.5 text-xs\""); + expect(recoverySource).not.toContain( + 'rounded-md p-1 hover:text-base-content hover:bg-surface-hover', + ); + expect(recoverySource).not.toContain( + 'const labelEvery = dayCount <= 7 ? 1 : dayCount <= 30 ? 3 : 10', + ); + expect(recoverySource).not.toContain('class="flex items-center gap-1"'); + expect(recoverySource).not.toContain( + 'class="inline-flex rounded border border-border bg-surface p-0.5 text-xs"', + ); expect(recoverySource).not.toContain("'font-semibold text-blue-700 dark:text-blue-300'"); expect(recoverySource).not.toContain('const rollupSubjectLabel ='); expect(recoverySource).not.toContain('const pointTimestampMs ='); @@ -554,7 +572,9 @@ describe('frontend resource type boundaries', () => { expect(recoverySource).not.toContain('Loading recovery points...'); expect(recoverySource).not.toContain('Failed to load protected items'); expect(recoverySource).not.toContain('Failed to load recovery points'); - expect(recoverySource).not.toContain('Pulse hasn’t observed any protected items for this org yet.'); + expect(recoverySource).not.toContain( + 'Pulse hasn’t observed any protected items for this org yet.', + ); expect(recoverySource).not.toContain('No recovery history matches your filters'); expect(recoverySource).not.toContain( 'Adjust your search, provider, method, status, or verification filters.', @@ -579,21 +599,11 @@ describe('frontend resource type boundaries', () => { 'export function recoveryDateKeyFromTimestamp', ); expect(recoveryDatePresentationSource).toContain('export function parseRecoveryDateKey'); - expect(recoveryDatePresentationSource).toContain( - 'export function getRecoveryPrettyDateLabel', - ); - expect(recoveryDatePresentationSource).toContain( - 'export function getRecoveryFullDateLabel', - ); - expect(recoveryDatePresentationSource).toContain( - 'export function getRecoveryCompactAxisLabel', - ); - expect(recoveryDatePresentationSource).toContain( - 'export function formatRecoveryTimeOnly', - ); - expect(recoveryDatePresentationSource).toContain( - 'export function getRecoveryNiceAxisMax', - ); + expect(recoveryDatePresentationSource).toContain('export function getRecoveryPrettyDateLabel'); + expect(recoveryDatePresentationSource).toContain('export function getRecoveryFullDateLabel'); + expect(recoveryDatePresentationSource).toContain('export function getRecoveryCompactAxisLabel'); + expect(recoveryDatePresentationSource).toContain('export function formatRecoveryTimeOnly'); + expect(recoveryDatePresentationSource).toContain('export function getRecoveryNiceAxisMax'); expect(recoveryActionPresentationSource).toContain( 'export function getRecoveryBreadcrumbLinkClass', ); @@ -681,9 +691,7 @@ describe('frontend resource type boundaries', () => { expect(recoveryTablePresentationSource).toContain( 'export function getRecoverySubjectTypeLabel', ); - expect(recoveryTablePresentationSource).toContain( - 'export function getRecoveryRollupIssueTone', - ); + expect(recoveryTablePresentationSource).toContain('export function getRecoveryRollupIssueTone'); expect(recoveryTablePresentationSource).toContain( 'export function getRecoveryRollupAgeTextClass', ); @@ -705,9 +713,7 @@ describe('frontend resource type boundaries', () => { expect(recoverySummaryPresentationSource).toContain( 'export const RECOVERY_SUMMARY_TIME_RANGES', ); - expect(recoverySummaryPresentationSource).toContain( - 'export const RECOVERY_FRESHNESS_BUCKETS', - ); + expect(recoverySummaryPresentationSource).toContain('export const RECOVERY_FRESHNESS_BUCKETS'); expect(recoverySummaryPresentationSource).toContain( 'export function getRecoveryAttentionChipClass', ); @@ -716,8 +722,8 @@ describe('frontend resource type boundaries', () => { ); expect(recoverySummarySource).toContain('getRecoveryAttentionChipClass'); expect(recoverySummarySource).toContain('getRecoveryAttentionDotClass'); - expect(recoverySummarySource).not.toContain("function getAttentionChipClass("); - expect(recoverySummarySource).not.toContain("function getAttentionDotClass("); + expect(recoverySummarySource).not.toContain('function getAttentionChipClass('); + expect(recoverySummarySource).not.toContain('function getAttentionDotClass('); expect(dashboardHelpersSource).toContain("from '@/utils/dashboardMetricPresentation'"); expect(dashboardHelpersSource).not.toContain('export function statusBadgeClass'); expect(dashboardHelpersSource).not.toContain('export function priorityBadgeClass'); @@ -731,9 +737,7 @@ describe('frontend resource type boundaries', () => { expect(trendChartsSource).toContain('getDashboardTrendErrorState'); expect(trendChartsSource).not.toContain('RESOURCE_COLORS'); expect(trendChartsSource).not.toContain('Unable to load trends'); - expect(dashboardTrendPresentationSource).toContain( - 'export function getDashboardTrendColor', - ); + expect(dashboardTrendPresentationSource).toContain('export function getDashboardTrendColor'); expect(dashboardTrendPresentationSource).toContain( 'export function getDashboardTrendErrorState', ); @@ -742,9 +746,7 @@ describe('frontend resource type boundaries', () => { expect(systemSettingsPresentationSource).toContain( 'export function getBackupIntervalSelectValue', ); - expect(systemSettingsPresentationSource).toContain( - 'export function getBackupIntervalSummary', - ); + expect(systemSettingsPresentationSource).toContain('export function getBackupIntervalSummary'); expect(systemSettingsPresentationSource).toContain('export const COMMON_DISCOVERY_SUBNETS'); expect(reportingPanelSource).toContain('@/utils/upgradePresentation'); expect(reportingPanelSource).toContain('getUpgradeActionButtonClass'); @@ -772,13 +774,13 @@ describe('frontend resource type boundaries', () => { expect(organizationAccessPanelSource).toContain('@/utils/organizationRolePresentation'); expect(organizationAccessPanelSource).toContain('ORGANIZATION_MEMBER_ROLE_OPTIONS'); expect(organizationAccessPanelSource).not.toContain( - "const roleOptions: Array<{ value: OrganizationRole; label: string }> = [", + 'const roleOptions: Array<{ value: OrganizationRole; label: string }> = [', ); expect(organizationSharingPanelSource).toContain('@/utils/organizationRolePresentation'); expect(organizationSharingPanelSource).toContain('ORGANIZATION_SHARE_ROLE_OPTIONS'); expect(organizationSharingPanelSource).toContain('normalizeOrganizationShareRole'); expect(organizationSharingPanelSource).not.toContain( - "const accessRoleOptions: Array<{ value: ShareAccessRole; label: string }> = [", + 'const accessRoleOptions: Array<{ value: ShareAccessRole; label: string }> = [', ); expect(organizationSharingPanelSource).not.toContain('const normalizeShareRole ='); expect(organizationRolePresentationSource).toContain( @@ -819,9 +821,7 @@ describe('frontend resource type boundaries', () => { expect(organizationSharingPanelSource).toContain( 'getOrganizationShareTargetOrgRequiredMessage', ); - expect(organizationSharingPanelSource).toContain( - 'getOrganizationShareCreateSuccessMessage', - ); + expect(organizationSharingPanelSource).toContain('getOrganizationShareCreateSuccessMessage'); expect(organizationAccessPanelSource).not.toContain('No organization members found.'); expect(organizationOverviewPanelSource).not.toContain('No members found.'); expect(organizationSharingPanelSource).not.toContain('No outgoing shares configured.'); @@ -847,7 +847,7 @@ describe('frontend resource type boundaries', () => { 'export function getOrganizationSettingsLoadErrorMessage', ); expect(organizationSettingsPresentationSource).toContain( - "export type OrganizationSettingsLoadContext =", + 'export type OrganizationSettingsLoadContext =', ); expect(organizationSettingsPresentationSource).toContain( 'export const ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE', @@ -884,12 +884,8 @@ describe('frontend resource type boundaries', () => { expect(userAssignmentsPanelSource).not.toContain('Users sync on first login'); expect(rbacPresentationSource).toContain('export function getRBACFeatureGateCopy'); expect(rbacPresentationSource).toContain('export function getRolesEmptyState'); - expect(rbacPresentationSource).toContain( - 'export function getUserAssignmentsEmptyStateCopy', - ); - expect(agentProfilesPresentationSource).toContain( - 'export function getAgentProfilesEmptyState', - ); + expect(rbacPresentationSource).toContain('export function getUserAssignmentsEmptyStateCopy'); + expect(agentProfilesPresentationSource).toContain('export function getAgentProfilesEmptyState'); expect(agentProfilesPresentationSource).toContain( 'export function getAgentProfileAssignmentsEmptyState', ); @@ -932,9 +928,7 @@ describe('frontend resource type boundaries', () => { expect(dashboardAlertPresentationSource).toContain( 'export function getDashboardAlertSummaryText', ); - expect(dashboardAlertPresentationSource).toContain( - 'export const DASHBOARD_ALERTS_EMPTY_STATE', - ); + expect(dashboardAlertPresentationSource).toContain('export const DASHBOARD_ALERTS_EMPTY_STATE'); expect(storagePanelSource).toContain('@/utils/dashboardStoragePresentation'); expect(storagePanelSource).not.toContain('No storage resources'); expect(recoveryStatusPanelSource).toContain('@/utils/dashboardRecoveryPresentation'); @@ -1007,14 +1001,18 @@ describe('frontend resource type boundaries', () => { expect(diskDetailSource).not.toContain('function extractDiskData('); expect(diskDetailSource).not.toContain('const getMetricResourceId = () => {'); expect(diskDetailSource).not.toContain('function attrColor('); - expect(diskDetailSource).not.toContain('flex flex-wrap items-end justify-between gap-3 border-b border-border-subtle pb-3'); + expect(diskDetailSource).not.toContain( + 'flex flex-wrap items-end justify-between gap-3 border-b border-border-subtle pb-3', + ); expect(diskDetailSource).not.toContain('class="relative"'); expect(useDiskDetailModelSource).toContain('extractPhysicalDiskPresentationData'); expect(useDiskDetailModelSource).toContain('resolvePhysicalDiskMetricResourceId'); expect(diskLiveMetricSource).toContain('useDiskLiveMetricModel'); expect(diskLiveMetricSource).not.toContain('const latestMetric = createMemo(() => {'); expect(diskLiveMetricSource).not.toContain('const formatted = createMemo(() => {'); - expect(diskLiveMetricSource).not.toContain("if (v > 90) return 'text-red-600 dark:text-red-400 font-bold'"); + expect(diskLiveMetricSource).not.toContain( + "if (v > 90) return 'text-red-600 dark:text-red-400 font-bold'", + ); expect(useDiskLiveMetricModelSource).toContain('getDiskLiveMetricFormattedValue'); expect(useDiskLiveMetricModelSource).toContain('getDiskLiveMetricTextClass'); expect(storagePoolRowSource).toContain('getStoragePoolProtectionTextClass'); @@ -1028,27 +1026,13 @@ describe('frontend resource type boundaries', () => { expect(storagePoolRowSource).not.toContain('const platformLabel = createMemo('); expect(storagePoolRowSource).not.toContain('const hostLabel = createMemo('); expect(storagePoolRowSource).not.toContain('const topologyLabel = createMemo('); - expect(storagePoolRowPresentationSource).toContain( - 'export const buildStoragePoolRowModel', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getSourcePlatformPresentation', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getCompactStoragePoolProtectionLabel', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getCompactStoragePoolImpactLabel', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getCompactStoragePoolIssueLabel', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getCompactStoragePoolIssueSummary', - ); - expect(storagePoolRowPresentationSource).toContain( - 'getCompactStoragePoolProtectionTitle', - ); + expect(storagePoolRowPresentationSource).toContain('export const buildStoragePoolRowModel'); + expect(storagePoolRowPresentationSource).toContain('getSourcePlatformPresentation'); + expect(storagePoolRowPresentationSource).toContain('getCompactStoragePoolProtectionLabel'); + expect(storagePoolRowPresentationSource).toContain('getCompactStoragePoolImpactLabel'); + expect(storagePoolRowPresentationSource).toContain('getCompactStoragePoolIssueLabel'); + expect(storagePoolRowPresentationSource).toContain('getCompactStoragePoolIssueSummary'); + expect(storagePoolRowPresentationSource).toContain('getCompactStoragePoolProtectionTitle'); expect(storagePoolRowSource).not.toContain('const protectionTextClass ='); expect(storagePoolRowSource).not.toContain('const issueTextClass ='); expect(storagePoolRowSource).not.toContain('const compactProtection = createMemo(() => {'); @@ -1100,9 +1084,7 @@ describe('frontend resource type boundaries', () => { expect(storageRowPresentationSource).toContain( 'export function getStoragePoolProtectionTextClass', ); - expect(storageRowPresentationSource).toContain( - 'export function getStoragePoolIssueTextClass', - ); + expect(storageRowPresentationSource).toContain('export function getStoragePoolIssueTextClass'); expect(storageRowPresentationSource).toContain( 'export function getCompactStoragePoolProtectionLabel', ); @@ -1125,9 +1107,7 @@ describe('frontend resource type boundaries', () => { expect(storageGroupPresentationSource).toContain( 'export const getStorageGroupHealthCountPresentation', ); - expect(storageGroupPresentationSource).toContain( - 'export const getStorageGroupPoolCountLabel', - ); + expect(storageGroupPresentationSource).toContain('export const getStorageGroupPoolCountLabel'); expect(storageGroupPresentationSource).toContain( 'export const getStorageGroupUsagePercentLabel', ); @@ -1140,7 +1120,7 @@ describe('frontend resource type boundaries', () => { expect(zfsHealthMapSource).toContain('getZfsHealthMapErrorSummaryClass'); expect(zfsHealthMapSource).toContain('getZfsHealthMapMessageClass'); expect(zfsHealthMapSource).toContain('useZFSHealthMapModel'); - expect(zfsHealthMapSource).not.toContain("const getDeviceColor = (device: ZFSDevice) => {"); + expect(zfsHealthMapSource).not.toContain('const getDeviceColor = (device: ZFSDevice) => {'); expect(zfsHealthMapSource).not.toContain('const isResilvering = (device: ZFSDevice) => {'); expect(enhancedStorageBarSource).toContain('getZfsPoolStateTextClass'); expect(enhancedStorageBarSource).toContain('getZfsPoolErrorOverlayClass'); @@ -1154,15 +1134,15 @@ describe('frontend resource type boundaries', () => { expect(enhancedStorageBarSource).not.toContain('const isScrubbing = createMemo(() => {'); expect(enhancedStorageBarSource).not.toContain('const isResilvering = createMemo(() => {'); expect(enhancedStorageBarSource).not.toContain('const hasErrors = createMemo(() => {'); - expect(enhancedStorageBarSource).not.toContain('metric-text w-full h-5 flex items-center min-w-0'); + expect(enhancedStorageBarSource).not.toContain( + 'metric-text w-full h-5 flex items-center min-w-0', + ); expect(useEnhancedStorageBarModelSource).toContain('getStorageBarUsagePercent'); expect(useEnhancedStorageBarModelSource).toContain('getStorageBarLabel'); expect(useEnhancedStorageBarModelSource).toContain('getStorageBarTooltipRows'); expect(useEnhancedStorageBarModelSource).toContain('getStorageBarZfsSummary'); expect(storagePagePresentationSource).toContain('export const shouldShowCephSummaryCard'); - expect(storagePagePresentationSource).toContain( - 'export const getStoragePageBannerMessage', - ); + expect(storagePagePresentationSource).toContain('export const getStoragePageBannerMessage'); expect(storagePagePresentationSource).toContain('export const STORAGE_VIEW_OPTIONS'); expect(storagePagePresentationSource).toContain('export const STORAGE_POOL_TABLE_COLUMNS'); expect(storagePagePresentationSource).toContain( @@ -1214,7 +1194,9 @@ describe('frontend resource type boundaries', () => { expect(storageControlsSource).toContain('STORAGE_CONTROLS_NODE_DIVIDER_CLASS'); expect(storageControlsSource).toContain('DEFAULT_STORAGE_SORT_OPTIONS'); expect(storageControlsSource).not.toContain('STORAGE_VIEW_OPTIONS'); - expect(storageControlsSource).not.toContain("props.setSelectedNodeId(event.currentTarget.value)"); + expect(storageControlsSource).not.toContain( + 'props.setSelectedNodeId(event.currentTarget.value)', + ); expect(storageControlsSource).not.toContain('focus:ring-blue-500'); expect(useStorageControlsModelSource).toContain('STORAGE_VIEW_OPTIONS'); expect(useStorageControlsModelSource).toContain('handleNodeFilterChange'); @@ -1226,7 +1208,9 @@ describe('frontend resource type boundaries', () => { expect(storageFilterSource).not.toContain('const sourceOptions = (): StorageSourceOption[] =>'); expect(storageFilterSource).not.toContain('props.setSortKey(DEFAULT_STORAGE_SORT_KEY)'); expect(storageFilterSource).not.toContain("props.sortDirection() === 'asc' ? 'desc' : 'asc'"); - expect(storageFilterSource).not.toContain("props.sortDirection() === 'asc' ? 'rotate-180' : ''"); + expect(storageFilterSource).not.toContain( + "props.sortDirection() === 'asc' ? 'rotate-180' : ''", + ); expect(storageFilterSource).not.toContain('focus:ring-blue-500'); expect(useStorageFilterToolbarModelSource).toContain('countActiveStorageFilters'); expect(useStorageFilterToolbarModelSource).toContain('hasActiveStorageFilters'); @@ -1235,9 +1219,7 @@ describe('frontend resource type boundaries', () => { expect(useStorageFilterToolbarModelSource).toContain('getStorageSortDirectionTitle'); expect(useStorageFilterToolbarModelSource).toContain('getStorageSortDirectionIconClass'); expect(useStorageFilterToolbarModelSource).toContain('getNextStorageSortDirection'); - expect(storageFilterPresentationSource).toContain( - 'export const getStorageSortDirectionTitle', - ); + expect(storageFilterPresentationSource).toContain('export const getStorageSortDirectionTitle'); expect(storageFilterPresentationSource).toContain( 'export const getStorageSortDirectionIconClass', ); @@ -1251,7 +1233,9 @@ describe('frontend resource type boundaries', () => { expect(storagePageControlsSource).toContain('StorageControls'); expect(storagePageControlsSource).toContain('useStoragePageControlsModel'); expect(storagePageControlsSource).not.toContain('normalizeStorageSortKey'); - expect(storagePageControlsSource).not.toContain("props.view() === 'pools' ? props.storageFilterGroupBy : undefined"); + expect(storagePageControlsSource).not.toContain( + "props.view() === 'pools' ? props.storageFilterGroupBy : undefined", + ); expect(storagePageControlsSource).not.toContain("props.view() !== 'pools'"); expect(useStoragePageControlsModelSource).toContain('normalizeStorageSortKey'); expect(useStoragePageControlsModelSource).toContain("options.view() === 'pools'"); @@ -1269,7 +1253,9 @@ describe('frontend resource type boundaries', () => { expect(storagePageBannersSource).toContain('StoragePageBanner'); expect(storagePageBannersSource).toContain('useStoragePageBannersModel'); expect(storagePageBannersSource).not.toContain("props.kind() === 'reconnecting'"); - expect(useStoragePageBannersModelSource).toContain("kind === 'reconnecting' || kind === 'disconnected'"); + expect(useStoragePageBannersModelSource).toContain( + "kind === 'reconnecting' || kind === 'disconnected'", + ); expect(storagePageSummarySource).toContain('export const StoragePageSummary'); expect(storagePageSummarySource).toContain('useStoragePageSummary'); expect(storagePageSummarySource).toContain('StorageSummary'); @@ -1277,9 +1263,15 @@ describe('frontend resource type boundaries', () => { expect(storageCephSummaryCardSource).toContain('CEPH_SUMMARY_CARD_GRID_CLASS'); expect(storageCephSummaryCardSource).toContain('CEPH_SUMMARY_CARD_HEALTH_BADGE_CLASS'); expect(storageCephSummaryCardSource).not.toContain('const summary = () => props.summary'); - expect(storageCephSummaryCardSource).not.toContain('rounded-md border border-border bg-surface p-3'); - expect(storageCephSummaryCardSource).not.toContain('text-[11px] text-muted truncate max-w-[240px]'); - expect(storageCephSummaryCardSource).not.toContain('flex flex-wrap items-center justify-between gap-3'); + expect(storageCephSummaryCardSource).not.toContain( + 'rounded-md border border-border bg-surface p-3', + ); + expect(storageCephSummaryCardSource).not.toContain( + 'text-[11px] text-muted truncate max-w-[240px]', + ); + expect(storageCephSummaryCardSource).not.toContain( + 'flex flex-wrap items-center justify-between gap-3', + ); expect(useStorageCephSummaryCardModelSource).toContain('getCephSummaryHeaderPresentation'); expect(useStorageCephSummaryCardModelSource).toContain('getCephSummaryClusterCards'); expect(storageContentCardSource).toContain('export const StorageContentCard'); @@ -1287,8 +1279,12 @@ describe('frontend resource type boundaries', () => { expect(storageContentCardSource).toContain('DiskList'); expect(storageContentCardSource).toContain('StoragePoolsTable'); expect(storageContentCardSource).toContain('STORAGE_CONTENT_CARD_HEADER_CLASS'); - expect(storageContentCardSource).not.toContain("props.selectedNodeId() === 'all' ? null : props.selectedNodeId()"); - expect(storageContentCardSource).not.toContain('border-b border-border bg-surface-hover px-3 py-2'); + expect(storageContentCardSource).not.toContain( + "props.selectedNodeId() === 'all' ? null : props.selectedNodeId()", + ); + expect(storageContentCardSource).not.toContain( + 'border-b border-border bg-surface-hover px-3 py-2', + ); expect(useStorageContentCardModelSource).toContain('getStorageTableHeading'); expect(useStorageContentCardModelSource).toContain("options.view() === 'disks'"); expect(useStorageCephSectionModelSource).toContain('shouldShowCephSummaryCard'); @@ -1309,9 +1305,7 @@ describe('frontend resource type boundaries', () => { expect(storagePoolsTablePresentationSource).toContain( 'export const buildStoragePoolsTableRowModel', ); - expect(storagePoolsTablePresentationSource).toContain( - 'getStorageRowAlertPresentation', - ); + expect(storagePoolsTablePresentationSource).toContain('getStorageRowAlertPresentation'); expect(useStoragePoolsTableModelSource).toContain('buildStoragePoolsTableGroups'); expect(useStoragePoolsTableModelSource).toContain('buildStoragePoolsTableRowModel'); expect(useStoragePoolsTableModelSource).toContain('togglePool'); @@ -1329,18 +1323,22 @@ describe('frontend resource type boundaries', () => { expect(storagePageSource).not.toContain('const activeAlertsAccessor = () => {'); expect(storagePageSource).not.toContain('const adapterResources = createMemo(() => {'); expect(storagePageSource).not.toContain('const [summaryTimeRange, setSummaryTimeRange]'); - expect(storagePageSource).not.toContain("buildPath: buildStoragePath"); + expect(storagePageSource).not.toContain('buildPath: buildStoragePath'); expect(storagePageSource).not.toContain('const storageFilterGroupBy ='); expect(storagePageSource).not.toContain('const storageFilterStatus ='); expect(storagePageSource).not.toContain('const setStorageFilterStatus ='); expect(storagePageSource).not.toContain('const records = createMemo(() => buildStorageRecords'); expect(storagePageSource).not.toContain('const { byType } = useResources()'); - expect(storagePageSource).not.toContain('const storageRecoveryResources = useStorageRecoveryResources()'); + expect(storagePageSource).not.toContain( + 'const storageRecoveryResources = useStorageRecoveryResources()', + ); expect(storagePageSource).not.toContain('useStoragePageFilters({'); expect(storagePageSource).not.toContain('useStoragePageData({'); expect(storagePageSource).not.toContain('useStoragePageResources()'); expect(storagePageSource).not.toContain('fields: {'); - expect(storagePageSource).not.toContain('rounded border border-amber-300 bg-amber-100 px-2 py-1'); + expect(storagePageSource).not.toContain( + 'rounded border border-amber-300 bg-amber-100 px-2 py-1', + ); expect(storagePageSource).not.toContain('flex flex-wrap items-center justify-between gap-3'); expect(storagePageSource).not.toContain(''); expect(storagePageSource).not.toContain(' { expect(storageRowAlertPresentationSource).toContain( 'export const getStorageRowAlertPresentation', ); - expect(diskLiveMetricPresentationSource).toContain( - 'export const getDiskLiveMetricTextClass', - ); + expect(diskLiveMetricPresentationSource).toContain('export const getDiskLiveMetricTextClass'); expect(diskLiveMetricPresentationSource).toContain( 'export const getDiskLiveMetricFormattedValue', ); - expect(diskPresentationSource).toContain( - 'export function extractPhysicalDiskPresentationData', - ); + expect(diskPresentationSource).toContain('export function extractPhysicalDiskPresentationData'); expect(diskPresentationSource).toContain('export function matchesPhysicalDiskSearch'); expect(diskPresentationSource).toContain('export function comparePhysicalDiskPresentation'); expect(diskPresentationSource).toContain( @@ -1365,14 +1359,20 @@ describe('frontend resource type boundaries', () => { expect(diskPresentationSource).toContain('export function filterAndSortPhysicalDisks'); expect(diskDetailPresentationSource).toContain('export function getDiskDetailAttributeCards'); expect(diskDetailPresentationSource).toContain('export function getDiskDetailHistoryCharts'); - expect(diskDetailPresentationSource).toContain('export const DISK_DETAIL_HISTORY_RANGE_OPTIONS'); + expect(diskDetailPresentationSource).toContain( + 'export const DISK_DETAIL_HISTORY_RANGE_OPTIONS', + ); expect(diskDetailPresentationSource).toContain('export const DISK_DETAIL_LIVE_CHARTS'); expect(storageDetailPresentationSource).toContain('export const STORAGE_DETAIL_CARD_CLASS'); expect(storageDetailPresentationSource).toContain('export const STORAGE_DETAIL_SELECT_CLASS'); expect(storageDetailPresentationSource).toContain('export const STORAGE_DETAIL_EMPTY_CLASS'); expect(storageDetailPresentationSource).toContain('export const STORAGE_DETAIL_ROW_CLASS'); - expect(storageDetailPresentationSource).toContain('export const STORAGE_DISK_DETAIL_HEADER_CLASS'); - expect(storageDetailPresentationSource).toContain('export const STORAGE_DETAIL_FULL_WIDTH_ROW_CLASS'); + expect(storageDetailPresentationSource).toContain( + 'export const STORAGE_DISK_DETAIL_HEADER_CLASS', + ); + expect(storageDetailPresentationSource).toContain( + 'export const STORAGE_DETAIL_FULL_WIDTH_ROW_CLASS', + ); expect(storageDetailKeyValueRowSource).toContain('STORAGE_DETAIL_KEY_VALUE_ROW_CLASS'); expect(storageDetailMetricCardSource).toContain('STORAGE_DETAIL_CARD_CLASS'); expect(storagePoolDetailSource).toContain('STORAGE_DETAIL_ROW_CLASS'); @@ -1388,15 +1388,11 @@ describe('frontend resource type boundaries', () => { expect(zfsHealthMapPresentationSource).toContain( 'export const getZfsHealthMapTooltipPresentation', ); - expect(zfsHealthMapPresentationSource).toContain( - 'export const getZfsHealthMapDeviceClass', - ); + expect(zfsHealthMapPresentationSource).toContain('export const getZfsHealthMapDeviceClass'); expect(zfsHealthMapPresentationSource).toContain( 'export const getZfsHealthMapErrorSummaryClass', ); - expect(zfsHealthMapPresentationSource).toContain( - 'export const getZfsHealthMapMessageClass', - ); + expect(zfsHealthMapPresentationSource).toContain('export const getZfsHealthMapMessageClass'); expect(zfsHealthMapPresentationSource).toContain( 'export const ZFS_HEALTH_MAP_TOOLTIP_CARD_CLASS', ); @@ -1405,13 +1401,13 @@ describe('frontend resource type boundaries', () => { expect(useZFSHealthMapModelSource).toContain('getZfsHealthMapTooltipPresentation'); expect(zfsHealthMapSource).toContain('ZFS_HEALTH_MAP_TOOLTIP_CARD_CLASS'); expect(zfsHealthMapSource).not.toContain('fixed z-[9999] pointer-events-none'); - expect(zfsHealthMapSource).not.toContain('bg-surface text-base-content text-[10px] rounded-md shadow-sm px-2 py-1.5 min-w-[120px] border border-border'); + expect(zfsHealthMapSource).not.toContain( + 'bg-surface text-base-content text-[10px] rounded-md shadow-sm px-2 py-1.5 min-w-[120px] border border-border', + ); expect(cephSummaryCardPresentationSource).toContain( 'export const getCephSummaryHeaderPresentation', ); - expect(cephSummaryCardPresentationSource).toContain( - 'export const getCephSummaryClusterCards', - ); + expect(cephSummaryCardPresentationSource).toContain('export const getCephSummaryClusterCards'); expect(cephSummaryCardPresentationSource).toContain( 'export const CEPH_SUMMARY_CARD_GRID_CLASS', ); @@ -1430,9 +1426,7 @@ describe('frontend resource type boundaries', () => { expect(useDiskListModelSource).toContain('buildPhysicalDiskPresentationDataMap'); expect(useDiskListModelSource).toContain('filterAndSortPhysicalDisks'); expect(useDiskListModelSource).toContain('matchesPhysicalDiskNode'); - expect(storageAdaptersSource).toContain( - "from './resourceStoragePresentation'", - ); + expect(storageAdaptersSource).toContain("from './resourceStoragePresentation'"); expect(storageAdaptersSource).toContain("from './resourceStorageMapping'"); expect(storageAdaptersSource).toContain("from './storageAdapterCore'"); expect(storageAdaptersSource).toContain('asNumberOrNull'); @@ -1529,21 +1523,13 @@ describe('frontend resource type boundaries', () => { expect(diskDetailPresentationSource).toContain( 'export function getDiskAttributeValueTextClass', ); - expect(diskDetailPresentationSource).toContain( - 'export function getLinkedDiskHealthDotClass', - ); + expect(diskDetailPresentationSource).toContain('export function getLinkedDiskHealthDotClass'); expect(diskDetailPresentationSource).toContain( 'export function getLinkedDiskTemperatureTextClass', ); - expect(storagePoolDetailPresentationSource).toContain( - 'export function getZfsScanTextClass', - ); - expect(storagePoolDetailPresentationSource).toContain( - 'export function getZfsErrorTextClass', - ); - expect(storagePoolDetailPresentationSource).toContain( - 'export function getZfsErrorSummary', - ); + expect(storagePoolDetailPresentationSource).toContain('export function getZfsScanTextClass'); + expect(storagePoolDetailPresentationSource).toContain('export function getZfsErrorTextClass'); + expect(storagePoolDetailPresentationSource).toContain('export function getZfsErrorSummary'); expect(storagePoolDetailPresentationSource).toContain( 'export const STORAGE_POOL_DETAIL_HISTORY_RANGE_OPTIONS', ); @@ -1574,31 +1560,19 @@ describe('frontend resource type boundaries', () => { expect(storageRecordPresentationSource).toContain('export const getStorageRecordType'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordContent'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordStatus'); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordPlatformLabel', - ); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordPlatformLabel'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordHostLabel'); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordTopologyLabel', - ); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordTopologyLabel'); expect(storageRecordPresentationSource).toContain( 'export const getStorageRecordProtectionLabel', ); expect(storageRecordPresentationSource).toContain('export const getStorageRecordIssueLabel'); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordIssueSummary', - ); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordImpactSummary', - ); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordActionSummary', - ); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordIssueSummary'); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordImpactSummary'); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordActionSummary'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordShared'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordNodeLabel'); - expect(storageRecordPresentationSource).toContain( - 'export const getStorageRecordUsagePercent', - ); + expect(storageRecordPresentationSource).toContain('export const getStorageRecordUsagePercent'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordZfsPool'); expect(storageRecordPresentationSource).toContain('export const getStorageRecordStats'); expect(resourceStoragePresentationSource).toContain( @@ -1627,27 +1601,19 @@ describe('frontend resource type boundaries', () => { ); expect(resourceStorageMappingSource).toContain('export type ResourceStorageMeta'); expect(resourceStorageMappingSource).toContain('export const readResourceStorageMeta'); - expect(resourceStorageMappingSource).toContain( - 'export const resolveResourceStorageContent', - ); + expect(resourceStorageMappingSource).toContain('export const resolveResourceStorageContent'); expect(resourceStorageMappingSource).toContain( 'export const getStorageCapabilitiesForResource', ); - expect(resourceStorageMappingSource).toContain( - 'export const getStorageCategoryFromType', - ); + expect(resourceStorageMappingSource).toContain('export const getStorageCategoryFromType'); expect(storageAdapterCoreSource).toContain('export const asNumberOrNull'); expect(storageAdapterCoreSource).toContain('export const dedupe'); expect(storageAdapterCoreSource).toContain('export const getStringArray'); expect(storageAdapterCoreSource).toContain('export const canonicalStorageIdentityKey'); expect(storageAdapterCoreSource).toContain('export const buildStorageSource'); expect(storageAdapterCoreSource).toContain('export const buildStorageCapacity'); - expect(storageAdapterCoreSource).toContain( - 'export const metricsTargetForStorageResource', - ); - expect(storageAdapterCoreSource).toContain( - 'export const normalizeStorageResourceHealth', - ); + expect(storageAdapterCoreSource).toContain('export const metricsTargetForStorageResource'); + expect(storageAdapterCoreSource).toContain('export const normalizeStorageResourceHealth'); expect(storageAlertStateSource).toContain('export type StorageAlertRowState'); expect(storageAlertStateSource).toContain('export const EMPTY_STORAGE_ALERT_STATE'); expect(storageAlertStateSource).toContain('export const asStorageAlertRecord'); @@ -1719,7 +1685,7 @@ describe('frontend resource type boundaries', () => { expect(k8sDeploymentsDrawerSource).not.toContain('const statusTone ='); expect(k8sDeploymentsDrawerSource).not.toContain('Desired state controllers (not Pods)'); expect(k8sDeploymentsDrawerSource).not.toContain('Search deployments...'); - expect(k8sDeploymentsDrawerSource).not.toContain("label=\"Namespace\""); + expect(k8sDeploymentsDrawerSource).not.toContain('label="Namespace"'); expect(k8sDeploymentsDrawerSource).not.toContain('All namespaces'); expect(k8sDeploymentsDrawerSource).not.toContain('Open Pods'); expect(k8sDeploymentsDrawerSource).not.toContain('Loading deployments...'); @@ -1755,9 +1721,7 @@ describe('frontend resource type boundaries', () => { expect(k8sNamespacePresentationSource).toContain( 'export function getK8sNamespacesDrawerPresentation', ); - expect(k8sNamespacePresentationSource).toContain( - 'export function getK8sNamespacesEmptyState', - ); + expect(k8sNamespacePresentationSource).toContain('export function getK8sNamespacesEmptyState'); expect(k8sNamespacePresentationSource).toContain( 'export function getK8sNamespacesLoadingState', ); @@ -1794,9 +1758,7 @@ describe('frontend resource type boundaries', () => { 'export const getOrganizationBillingLicenseStatusLabel', ); expect(licensePresentationSource).toContain('export const getBillingAdminTrialStatus'); - expect(licensePresentationSource).toContain( - 'export const getBillingAdminOrganizationBadges', - ); + expect(licensePresentationSource).toContain('export const getBillingAdminOrganizationBadges'); expect(licensePresentationSource).toContain( 'export const getBillingAdminStateUpdateSuccessMessage', ); @@ -1819,7 +1781,9 @@ describe('frontend resource type boundaries', () => { expect(securityWarningSource).toContain('warningPresentation().background'); expect(securityWarningSource).toContain('warningPresentation().border'); expect(securityWarningSource).not.toContain('bg-yellow-50 dark:bg-yellow-900'); - expect(securityWarningSource).not.toContain("status()!.credentialsEncrypted ? 'text-green-600' : 'text-red-600'"); + expect(securityWarningSource).not.toContain( + "status()!.credentialsEncrypted ? 'text-green-600' : 'text-red-600'", + ); expect(findingsPanelSource).toContain('getFindingStatusBadgeClasses'); expect(findingsPanelSource).toContain('getFindingStatusLabel'); expect(findingsPanelSource).toContain('getFindingSeveritySortOrder'); @@ -1842,31 +1806,17 @@ describe('frontend resource type boundaries', () => { ); expect(findingsPanelSource).not.toContain('No active findings'); expect(findingsPanelSource).not.toContain('No pending approvals.'); - expect(aiFindingPresentationSource).toContain( - 'export const getFindingStatusBadgeClasses', - ); + expect(aiFindingPresentationSource).toContain('export const getFindingStatusBadgeClasses'); expect(aiFindingPresentationSource).toContain('export const getFindingStatusLabel'); - expect(aiFindingPresentationSource).toContain( - 'export const getFindingSeveritySortOrder', - ); - expect(aiFindingPresentationSource).toContain( - 'export const getFindingSeverityCompactLabel', - ); + expect(aiFindingPresentationSource).toContain('export const getFindingSeveritySortOrder'); + expect(aiFindingPresentationSource).toContain('export const getFindingSeverityCompactLabel'); expect(aiFindingPresentationSource).toContain( 'export const getInvestigationOutcomeBadgeClasses', ); - expect(aiFindingPresentationSource).toContain( - 'export const getInvestigationOutcomeLabel', - ); - expect(aiFindingPresentationSource).toContain( - 'export const getInvestigationStatusLabel', - ); - expect(aiFindingPresentationSource).toContain( - 'export const getInvestigationOutcomeSortOrder', - ); - expect(aiFindingPresentationSource).toContain( - 'export const hasFindingInvestigationDetails', - ); + expect(aiFindingPresentationSource).toContain('export const getInvestigationOutcomeLabel'); + expect(aiFindingPresentationSource).toContain('export const getInvestigationStatusLabel'); + expect(aiFindingPresentationSource).toContain('export const getInvestigationOutcomeSortOrder'); + expect(aiFindingPresentationSource).toContain('export const hasFindingInvestigationDetails'); expect(aiFindingPresentationSource).toContain('export const getFindingResolutionReason'); expect(aiFindingPresentationSource).toContain('export const buildFindingFilterOptions'); expect(aiFindingPresentationSource).toContain('export const getFindingEmptyStateCopy'); @@ -1892,9 +1842,7 @@ describe('frontend resource type boundaries', () => { expect(securityScorePresentationSource).toContain( 'export function getSecurityFeatureCardPresentation', ); - expect(securityScorePresentationSource).toContain( - 'export function getSecurityPostureItems', - ); + expect(securityScorePresentationSource).toContain('export function getSecurityPostureItems'); expect(securityScorePresentationSource).toContain( 'export function getSecurityNetworkAccessSubtitle', ); @@ -1907,9 +1855,7 @@ describe('frontend resource type boundaries', () => { expect(securityAuthPanelSource).not.toContain( 'Authentication is currently disabled. Set up password authentication to protect your Pulse instance.', ); - expect(securityAuthPanelSource).not.toContain( - 'Security Configured - Restart Required', - ); + expect(securityAuthPanelSource).not.toContain('Security Configured - Restart Required'); expect(securityAuthPresentationSource).toContain( 'export const SECURITY_AUTH_DISABLED_PANEL_TITLE', ); @@ -1930,9 +1876,7 @@ describe('frontend resource type boundaries', () => { expect(environmentLockPresentationSource).toContain( 'export const ENVIRONMENT_LOCK_BADGE_CLASS', ); - expect(environmentLockPresentationSource).toContain( - 'export function getEnvironmentLockTitle', - ); + expect(environmentLockPresentationSource).toContain('export function getEnvironmentLockTitle'); expect(environmentLockPresentationSource).toContain( 'export const ENVIRONMENT_LOCK_BUTTON_TITLE', ); @@ -1947,15 +1891,11 @@ describe('frontend resource type boundaries', () => { expect(resourceDetailDrawerSource).toContain('getResourceChangeSourceAdapterPresentation'); expect(resourceChangeSummarySource).toContain('getResourceChangeKindPresentation'); expect(resourceChangeSummarySource).toContain('getResourceChangeSourceTypePresentation'); - expect(resourceChangeSummarySource).toContain( - 'getResourceChangeSourceAdapterPresentation', - ); + expect(resourceChangeSummarySource).toContain('getResourceChangeSourceAdapterPresentation'); expect(resourceDetailDrawerSource).not.toContain('healthToneClass('); expect(resourceDetailDrawerSource).not.toContain('normalizeHealthLabel('); expect(resourceChangePresentationSource).toContain('getResourceChangeKindPresentation'); - expect(resourceChangePresentationSource).toContain( - 'getResourceChangeSourceTypePresentation', - ); + expect(resourceChangePresentationSource).toContain('getResourceChangeSourceTypePresentation'); expect(resourceChangePresentationSource).toContain( 'getResourceChangeSourceAdapterPresentation', ); @@ -1969,21 +1909,29 @@ describe('frontend resource type boundaries', () => { expect(resourceDetailDrawerSource).toContain('formatIdentifierLabel'); expect(resourceChangePresentationSource).toContain('humanizeToken'); expect(textPresentationSource).toContain('humanizeArrowDelimitedLabel'); - expect(resourceCorrelationPresentationSource).not.toContain('formatResourceCorrelationEndpointLabel'); expect(resourceCorrelationPresentationSource).not.toContain( - "replace(/\\s*->\\s*/g, ' → ')", + 'formatResourceCorrelationEndpointLabel', ); + expect(resourceCorrelationPresentationSource).not.toContain("replace(/\\s*->\\s*/g, ' → ')"); expect(textPresentationSource).toContain('humanizeToken'); expect(textPresentationSource).toContain('formatIdentifierLabel'); expect(resourceCorrelationPresentationSource).not.toContain('formatTrimmedLabel'); expect(swarmServicesDrawerSource).toContain('asTrimmedString'); - expect(swarmServicesDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();"); + expect(swarmServicesDrawerSource).not.toContain( + "const normalize = (value?: string | null) => (value || '').trim();", + ); expect(pmgInstanceDrawerSource).toContain('asTrimmedString'); - expect(pmgInstanceDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();"); + expect(pmgInstanceDrawerSource).not.toContain( + "const normalize = (value?: string | null) => (value || '').trim();", + ); expect(k8sNamespacesDrawerSource).toContain('asTrimmedString'); - expect(k8sNamespacesDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();"); + expect(k8sNamespacesDrawerSource).not.toContain( + "const normalize = (value?: string | null) => (value || '').trim();", + ); expect(k8sDeploymentsDrawerSource).toContain('asTrimmedString'); - expect(k8sDeploymentsDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();"); + expect(k8sDeploymentsDrawerSource).not.toContain( + "const normalize = (value?: string | null) => (value || '').trim();", + ); expect(messageItemSource).toContain('formatIdentifierLabel'); expect(toolExecutionBlockSource).toContain('formatIdentifierLabel'); expect(aiChatSource).toContain('formatIdentifierLabel'); @@ -2000,18 +1948,22 @@ describe('frontend resource type boundaries', () => { expect(aiFindingPresentationSource).not.toContain("replace(/_/g, ' ')"); expect(patrolRunPresentationSource).toContain('formatIdentifierLabel'); expect(patrolRunPresentationSource).not.toContain("normalized.replace(/_/g, ' ')"); - expect(patrolRunPresentationSource).not.toContain("normalized ? normalized.replace(/_/g, ' ') : 'unknown'"); + expect(patrolRunPresentationSource).not.toContain( + "normalized ? normalized.replace(/_/g, ' ') : 'unknown'", + ); expect(useChatSource).toContain('normalizeChatToolName'); expect(useChatSource).not.toContain("replace(/^(pulse_)+/, '')"); expect(aiChatSource).toContain('normalizeChatMentionKeyPart'); - expect(aiChatSource).not.toContain("const normalizeMentionKeyPart ="); + expect(aiChatSource).not.toContain('const normalizeMentionKeyPart ='); expect(chatIdentifiersSource).toContain('normalizeChatMentionKeyPart'); expect(chatIdentifiersSource).toContain('normalizeChatToolName'); expect(infrastructureSummarySource).toContain('getNormalizedIdentityLookupVariants'); expect(infrastructureSummaryTableSource).toContain('getNormalizedIdentityLookupVariants'); expect(resourceIdentitySource).toContain('getNormalizedIdentityLookupVariants'); expect(stringUtilsSource).toContain('export const asTrimmedString'); - expect(resourceIdentitySource).not.toContain('const asTrimmedString = (value: unknown): string | undefined => {'); + expect(resourceIdentitySource).not.toContain( + 'const asTrimmedString = (value: unknown): string | undefined => {', + ); expect(infrastructureSummarySource).not.toContain( 'const asTrimmedString = (value: unknown): string | null => {', ); @@ -2171,9 +2123,7 @@ describe('frontend resource type boundaries', () => { expect(nodeModalPresentationSource).toContain('export function getNodeGuestUrlPlaceholder'); expect(nodeModalPresentationSource).toContain('export function getNodeUsernamePlaceholder'); expect(nodeModalPresentationSource).toContain('export function getNodeTokenIdPlaceholder'); - expect(nodeModalPresentationSource).toContain( - 'export function getNodeMonitoringCoverageCopy', - ); + expect(nodeModalPresentationSource).toContain('export function getNodeMonitoringCoverageCopy'); expect(nodeModalPresentationSource).toContain( 'export function getTemperatureMonitoringLockedCopy', ); @@ -2196,13 +2146,9 @@ describe('frontend resource type boundaries', () => { expect(cephPageSource).not.toContain('No pools match "'); expect(storageDomainSource).toContain('export const getCephServiceStatusPresentation'); expect(storageDomainSource).toContain('export const getCephLoadingStatePresentation'); - expect(storageDomainSource).toContain( - 'export const getCephDisconnectedStatePresentation', - ); + expect(storageDomainSource).toContain('export const getCephDisconnectedStatePresentation'); expect(storageDomainSource).toContain('export const getCephNoClustersStatePresentation'); - expect(storageDomainSource).toContain( - 'export const getCephPoolsSearchEmptyStatePresentation', - ); + expect(storageDomainSource).toContain('export const getCephPoolsSearchEmptyStatePresentation'); expect(cephServiceIconSource).toContain('export const CephServiceIcon'); expect(deployStatusBadgeSource).toContain('getDeployStatusPresentation'); expect(deployStatusBadgeSource).not.toContain('const statusConfig: Record { expect(deployFlowPresentationSource).toContain( 'export function getDeployCandidatesLoadingState', ); - expect(deployFlowPresentationSource).toContain( - 'export function getDeployNoSourceAgentsState', - ); - expect(deployFlowPresentationSource).toContain( - 'export function getDeployNoCandidatesState', - ); + expect(deployFlowPresentationSource).toContain('export function getDeployNoSourceAgentsState'); + expect(deployFlowPresentationSource).toContain('export function getDeployNoCandidatesState'); expect(deployFlowPresentationSource).toContain( 'export function getDeployInstallCommandLoadingState', ); expect(deployStatusPresentationSource).toContain('export const getDeployStatusPresentation'); expect(alertHistoryTabSource).toContain('getAlertIncidentStatusPresentation'); expect(alertHistoryTabSource).toContain('getAlertIncidentLevelBadgeClass'); - expect(alertsPageSource).toContain("import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';"); + expect(alertsPageSource).toContain( + "import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';", + ); expect(alertsPageSource).not.toContain('getAlertDestinationsConfigLoadError'); expect(alertsConfigurationSurfaceSource).toContain('getAlertDestinationsConfigLoadError'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsWebhookLoadError'); @@ -2245,7 +2189,9 @@ describe('frontend resource type boundaries', () => { expect(alertDestinationsTabSource).toContain('getAlertDestinationsStatusLabel'); expect(alertDestinationsTabSource).toContain('getAlertWebhookTestSuccess'); expect(alertDestinationsTabSource).toContain('getAlertWebhookTestFailure'); - expect(alertsPageSource).toContain("import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';"); + expect(alertsPageSource).toContain( + "import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';", + ); expect(alertsPageSource).not.toContain('function HistoryTab('); expect(alertHistoryTabSource).toContain('getAlertHistoryStatusPresentation'); expect(alertHistoryTabSource).toContain('getAlertHistorySourcePresentation'); @@ -2292,11 +2238,19 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).toContain('getAlertsMobileTabClass'); expect(alertsPageSource).toContain('getAlertsTabTitle'); expect(alertsPageSource).toContain('getAlertsTabGroups'); - expect(alertsConfigurationSurfaceSource).toContain("import { ThresholdsTab } from './tabs/ThresholdsTab';"); - expect(alertsPageSource).not.toContain("import { ThresholdsTab } from '@/features/alerts/tabs/ThresholdsTab';"); - expect(alertsPageSource).not.toContain("import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';"); + expect(alertsConfigurationSurfaceSource).toContain( + "import { ThresholdsTab } from './tabs/ThresholdsTab';", + ); + expect(alertsPageSource).not.toContain( + "import { ThresholdsTab } from '@/features/alerts/tabs/ThresholdsTab';", + ); + expect(alertsPageSource).not.toContain( + "import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';", + ); expect(alertsPageSource).not.toContain('function ThresholdsTab('); - expect(alertThresholdsTabSource).toContain("import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';"); + expect(alertThresholdsTabSource).toContain( + "import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';", + ); expect(alertThresholdsTabSource).toContain('pmgThresholds={props.pmgThresholds}'); expect(thresholdsTableSource).toContain( "import { useThresholdsData } from '@/features/alerts/thresholds/hooks/useThresholdsData';", @@ -2336,9 +2290,7 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).not.toContain( "isAlertsActive() ? 'text-green-600 dark:text-green-400' : 'text-muted'", ); - expect(alertsPageSource).not.toContain( - "isAlertsActive() ? 'bg-blue-600' : 'bg-surface-hover'", - ); + expect(alertsPageSource).not.toContain("isAlertsActive() ? 'bg-blue-600' : 'bg-surface-hover'"); expect(alertsPageSource).not.toContain( 'Failed to load notification configuration. Your existing settings could not be retrieved.', ); @@ -2347,10 +2299,18 @@ describe('frontend resource type boundaries', () => { 'Saving now may overwrite your existing settings with defaults.', ); expect(alertsPageSource).not.toContain('Configure SMTP delivery for alert emails.'); - expect(alertsPageSource).not.toContain('Relay grouped alerts through Apprise via CLI or remote API.'); - expect(alertsPageSource).not.toContain('Choose how Pulse should execute Apprise notifications.'); - expect(alertsPageSource).not.toContain('Enter one Apprise URL per line. Commas are also supported.'); - expect(alertsPageSource).not.toContain('Optional: override the URLs defined on your Apprise API instance. Leave blank to use the server defaults.'); + expect(alertsPageSource).not.toContain( + 'Relay grouped alerts through Apprise via CLI or remote API.', + ); + expect(alertsPageSource).not.toContain( + 'Choose how Pulse should execute Apprise notifications.', + ); + expect(alertsPageSource).not.toContain( + 'Enter one Apprise URL per line. Commas are also supported.', + ); + expect(alertsPageSource).not.toContain( + 'Optional: override the URLs defined on your Apprise API instance. Leave blank to use the server defaults.', + ); expect(alertsPageSource).not.toContain('Enable Apprise notifications before sending a test.'); expect(alertsPageSource).not.toContain('Add at least one Apprise target to test CLI delivery.'); expect(alertsPageSource).not.toContain('Enter an Apprise API server URL to test API delivery.'); @@ -2360,10 +2320,10 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).not.toContain('Failed to send test notification'); expect(alertsPageSource).not.toContain('Test webhook sent successfully!'); expect(alertsPageSource).not.toContain('Failed to send test webhook'); - expect(alertsPageSource).not.toContain('Enable only when the Apprise API uses a self-signed certificate.'); expect(alertsPageSource).not.toContain( - "isAlertsActive() ? 'translate-x-5' : 'translate-x-0'", + 'Enable only when the Apprise API uses a self-signed certificate.', ); + expect(alertsPageSource).not.toContain("isAlertsActive() ? 'translate-x-5' : 'translate-x-0'"); expect(alertsPageSource).not.toContain( 'inline-flex items-center gap-2 rounded-full border border-blue-200', ); @@ -2427,12 +2387,8 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).not.toContain('• Quiet hours active from '); expect(alertsPageSource).not.toContain('• Suppressing '); expect(alertsPageSource).not.toContain('• Recovery notifications enabled when alerts clear'); - expect(alertsPageSource).not.toContain( - 'CPU, memory, disk, and network thresholds stay quiet.', - ); - expect(alertsPageSource).not.toContain( - 'Silence storage usage, disk health, and ZFS events.', - ); + expect(alertsPageSource).not.toContain('CPU, memory, disk, and network thresholds stay quiet.'); + expect(alertsPageSource).not.toContain('Silence storage usage, disk health, and ZFS events.'); expect(alertsPageSource).not.toContain( 'Skip connectivity and powered-off alerts during backups.', ); @@ -2502,9 +2458,7 @@ describe('frontend resource type boundaries', () => { expect(alertTabsPresentationSource).toContain('export function getAlertsMobileTabClass'); expect(alertTabsPresentationSource).toContain('export function getAlertsTabTitle'); expect(alertTabsPresentationSource).toContain('export function getAlertsTabGroups'); - expect(alertGroupingPresentationSource).toContain( - 'export function getAlertGroupingCardClass', - ); + expect(alertGroupingPresentationSource).toContain('export function getAlertGroupingCardClass'); expect(alertGroupingPresentationSource).toContain( 'export function getAlertGroupingCheckboxClass', ); @@ -2522,7 +2476,9 @@ describe('frontend resource type boundaries', () => { expect(configuredNodeTablesSource).toContain('resolveConfiguredInstanceStatusIndicator'); expect(configuredNodeTablesSource).not.toContain("'monitorVMs' in node"); expect(configuredNodeTablesSource).not.toContain("'monitorDatastores' in node"); - expect(configuredNodeTablesSource).not.toContain('const resolveConfiguredNodeStatusIndicator ='); + expect(configuredNodeTablesSource).not.toContain( + 'const resolveConfiguredNodeStatusIndicator =', + ); expect(configuredNodeTablesSource).not.toContain('const resolvePbsStatusIndicator ='); expect(configuredNodeTablesSource).not.toContain('const resolvePmgStatusIndicator ='); expect(configuredNodeCapabilityPresentationSource).toContain( @@ -2551,23 +2507,31 @@ describe('frontend resource type boundaries', () => { expect(ssoProvidersPanelSource).toContain('SSOProviderTypeIcon'); expect(ssoProvidersPanelSource).not.toContain('No SSO providers configured'); expect(ssoProvidersPanelSource).not.toContain('Loading SSO providers...'); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to load SSO providers')"); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to load SSO providers')", + ); expect(ssoProvidersPanelSource).not.toContain( "notificationStore.error('Failed to load provider details')", ); expect(ssoProvidersPanelSource).not.toContain("notificationStore.success('Provider deleted')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to delete provider')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.success('Connection test successful')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Failed to test connection')"); - expect(ssoProvidersPanelSource).not.toContain("notificationStore.error('Please enter an IdP Metadata URL')"); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to delete provider')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.success('Connection test successful')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Failed to test connection')", + ); + expect(ssoProvidersPanelSource).not.toContain( + "notificationStore.error('Please enter an IdP Metadata URL')", + ); expect(ssoProvidersPanelSource).not.toContain('provider.type.toUpperCase()'); expect(ssoProvidersPanelSource).not.toContain("provider.type === 'oidc' ? ("); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderTypeLabel'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderSummary'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderCardClass'); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOProviderAddButtonLabel', - ); + expect(ssoProviderPresentationSource).toContain('export function getSSOProviderAddButtonLabel'); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderModalTitle'); expect(ssoProviderPresentationSource).toContain( 'export function getSSOProviderEmptyStateTitle', @@ -2575,12 +2539,8 @@ describe('frontend resource type boundaries', () => { expect(ssoProviderPresentationSource).toContain( 'export function getSSOProviderEmptyStateDescription', ); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOProvidersLoadingState', - ); - expect(ssoProviderPresentationSource).toContain( - 'export function getSSOTestResultPresentation', - ); + expect(ssoProviderPresentationSource).toContain('export function getSSOProvidersLoadingState'); + expect(ssoProviderPresentationSource).toContain('export function getSSOTestResultPresentation'); expect(ssoProviderPresentationSource).toContain( 'export function getSSOCertificatePresentation', ); @@ -2618,9 +2578,7 @@ describe('frontend resource type boundaries', () => { expect(auditWebhookPresentationSource).toContain( 'export function getAuditWebhookEmptyStateCopy', ); - expect(auditWebhookPresentationSource).toContain( - 'export function getAuditWebhookLoadingState', - ); + expect(auditWebhookPresentationSource).toContain('export function getAuditWebhookLoadingState'); expect(auditLogPanelSource).toContain('getAuditLogLoadingState'); expect(auditLogPanelSource).toContain('getAuditLogEmptyState'); expect(auditLogPanelSource).not.toContain('No audit events found'); @@ -2639,9 +2597,7 @@ describe('frontend resource type boundaries', () => { expect(auditLogPresentationSource).toContain( 'export function getAuditVerificationBadgePresentation', ); - expect(auditLogPresentationSource).toContain( - 'export function getAuditEventStatusPresentation', - ); + expect(auditLogPresentationSource).toContain('export function getAuditEventStatusPresentation'); expect(auditLogPresentationSource).toContain('export const AUDIT_REFRESH_BUTTON_CLASS'); expect(auditLogPresentationSource).toContain('export const AUDIT_VERIFY_ALL_BUTTON_CLASS'); expect(auditLogPresentationSource).toContain('export const AUDIT_VERIFY_ROW_BUTTON_CLASS'); @@ -2740,23 +2696,17 @@ describe('frontend resource type boundaries', () => { expect(runToolCallTraceSource).toContain('getToolCallsLoadingState'); expect(runToolCallTraceSource).toContain('getToolCallsUnavailableState'); expect(runToolCallTraceSource).not.toContain('Loading tool calls...'); - expect(runToolCallTraceSource).not.toContain( - 'Tool call details not available for this run.', - ); + expect(runToolCallTraceSource).not.toContain('Tool call details not available for this run.'); expect(patrolEmptyStatePresentationSource).toContain( 'export function getInvestigationMessagesState', ); expect(patrolEmptyStatePresentationSource).toContain( 'export function getInvestigationSectionState', ); - expect(patrolEmptyStatePresentationSource).toContain( - 'export function getRunHistoryEmptyState', - ); + expect(patrolEmptyStatePresentationSource).toContain('export function getRunHistoryEmptyState'); expect(patrolRunPresentationSource).toContain('export function getRunHistoryLoadingState'); expect(patrolRunPresentationSource).toContain('export function getToolCallsLoadingState'); - expect(patrolRunPresentationSource).toContain( - 'export function getToolCallsUnavailableState', - ); + expect(patrolRunPresentationSource).toContain('export function getToolCallsUnavailableState'); expect(systemLogsPanelSource).toContain('getSystemLogLineClass'); expect(systemLogsPanelSource).toContain('getSystemLogStreamPresentation'); expect(systemLogsPanelSource).not.toContain("log.includes('ERR')"); @@ -2919,7 +2869,9 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewTabSource).toContain('IncidentTimelinePanel'); expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterContainerClass'); expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterChipClass'); - expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterActionButtonClass'); + expect(alertIncidentEventFiltersSource).toContain( + 'getAlertIncidentEventFilterActionButtonClass', + ); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineLoadingState'); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineFilterEmptyState'); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineEmptyState'); @@ -2941,9 +2893,7 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewTabSource).toContain('getAlertOverviewPrimaryActionClass'); expect(alertOverviewTabSource).toContain('getAlertOverviewSecondaryActionClass'); expect(alertOverviewTabSource).not.toContain('Loading timeline...'); - expect(alertOverviewTabSource).not.toContain( - 'No timeline events match the selected filters.', - ); + expect(alertOverviewTabSource).not.toContain('No timeline events match the selected filters.'); expect(alertOverviewTabSource).not.toContain('No timeline events yet.'); expect(alertOverviewTabSource).not.toContain('No incident timeline available.'); expect(alertOverviewTabSource).not.toContain('Failed to load timeline.'); @@ -2953,9 +2903,7 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewPresentationSource).toContain( 'export function getAlertTimelineFilterEmptyState', ); - expect(alertOverviewPresentationSource).toContain( - 'export function getAlertTimelineEmptyState', - ); + expect(alertOverviewPresentationSource).toContain('export function getAlertTimelineEmptyState'); expect(alertOverviewPresentationSource).toContain( 'export function getAlertTimelineUnavailableState', ); @@ -3036,30 +2984,14 @@ describe('frontend resource type boundaries', () => { expect(alertIncidentTimelineEventCardSource).toContain( 'getAlertIncidentTimelineEventCardClass', ); - expect(alertIncidentTimelineEventCardSource).toContain( - 'getAlertIncidentTimelineMetaRowClass', - ); - expect(alertIncidentTimelineEventCardSource).toContain( - 'getAlertIncidentTimelineHeadingClass', - ); - expect(alertIncidentTimelineEventCardSource).toContain( - 'getAlertIncidentTimelineDetailClass', - ); - expect(alertIncidentTimelineEventCardSource).toContain( - 'getAlertIncidentTimelineCommandClass', - ); - expect(alertIncidentTimelineEventCardSource).toContain( - 'getAlertIncidentTimelineOutputClass', - ); - expect(alertOverviewPresentationSource).toContain( - 'export function getAlertHistoryEmptyState', - ); - expect(alertOverviewPresentationSource).toContain( - 'export function getAlertsPageHeaderMeta', - ); - expect(alertOverviewPresentationSource).toContain( - 'export function getAlertBucketCountLabel', - ); + expect(alertIncidentTimelineEventCardSource).toContain('getAlertIncidentTimelineMetaRowClass'); + expect(alertIncidentTimelineEventCardSource).toContain('getAlertIncidentTimelineHeadingClass'); + expect(alertIncidentTimelineEventCardSource).toContain('getAlertIncidentTimelineDetailClass'); + expect(alertIncidentTimelineEventCardSource).toContain('getAlertIncidentTimelineCommandClass'); + expect(alertIncidentTimelineEventCardSource).toContain('getAlertIncidentTimelineOutputClass'); + expect(alertOverviewPresentationSource).toContain('export function getAlertHistoryEmptyState'); + expect(alertOverviewPresentationSource).toContain('export function getAlertsPageHeaderMeta'); + expect(alertOverviewPresentationSource).toContain('export function getAlertBucketCountLabel'); expect(alertIncidentPresentationSource).toContain( 'export function getAlertResourceIncidentLoadingState', ); @@ -3124,9 +3056,7 @@ describe('frontend resource type boundaries', () => { expect(alertResourceTableSource).toContain('getAlertResourceTableCustomBadgeLabel'); expect(alertResourceTableSource).toContain('getAlertResourceTableMetricPlaceholder'); expect(alertResourceTableSource).toContain('getAlertResourceTableOverrideNotePlaceholder'); - expect(alertResourceTableSource).toContain( - 'getAlertResourceTableResetFactoryDefaultsLabel', - ); + expect(alertResourceTableSource).toContain('getAlertResourceTableResetFactoryDefaultsLabel'); expect(alertResourceTableSource).toContain('getAlertResourceTableAlertDelayLabel'); expect(alertResourceTableSource).toContain('getAlertResourceTableMetricInputTitle'); expect(alertResourceTableSource).toContain('getAlertResourceTableEditMetricTitle'); @@ -3188,9 +3118,7 @@ describe('frontend resource type boundaries', () => { expect(alertEmailPresentationSource).toContain( 'export function getAlertEmailRecipientsPlaceholder', ); - expect(alertEmailPresentationSource).toContain( - 'export function getAlertEmailTestButtonLabel', - ); + expect(alertEmailPresentationSource).toContain('export function getAlertEmailTestButtonLabel'); }); it('keeps alert bulk-edit dialog labels in a shared presentation utility', () => { @@ -3202,18 +3130,12 @@ describe('frontend resource type boundaries', () => { expect(bulkEditDialogSource).not.toContain('Unchanged'); expect(bulkEditDialogSource).not.toContain('Clear'); expect(bulkEditDialogSource).not.toContain('Apply to '); - expect(alertBulkEditPresentationSource).toContain( - 'export const ALERT_BULK_EDIT_DIALOG_TITLE', - ); + expect(alertBulkEditPresentationSource).toContain('export const ALERT_BULK_EDIT_DIALOG_TITLE'); expect(alertBulkEditPresentationSource).toContain( 'export function getAlertBulkEditDescription', ); - expect(alertBulkEditPresentationSource).toContain( - 'export function getAlertBulkEditApplyLabel', - ); - expect(alertBulkEditPresentationSource).toContain( - 'export function getAlertBulkEditOpenLabel', - ); + expect(alertBulkEditPresentationSource).toContain('export function getAlertBulkEditApplyLabel'); + expect(alertBulkEditPresentationSource).toContain('export function getAlertBulkEditOpenLabel'); }); it('keeps alert webhook service vocabulary and action copy in a shared presentation utility', () => { @@ -3251,9 +3173,7 @@ describe('frontend resource type boundaries', () => { expect(alertWebhookPresentationSource).toContain( 'export function getAlertWebhookServiceLabelFromTemplates', ); - expect(alertWebhookPresentationSource).toContain( - 'export function getAlertWebhookServices', - ); + expect(alertWebhookPresentationSource).toContain('export function getAlertWebhookServices'); expect(alertWebhookPresentationSource).not.toContain('ALERT_WEBHOOK_SERVICES'); expect(alertWebhookPresentationSource).toContain( 'export function getAlertWebhookMentionPlaceholderFromTemplates', @@ -3273,11 +3193,7 @@ describe('frontend resource type boundaries', () => { expect(alertWebhookPresentationSource).toContain( 'export function normalizeAlertWebhookCustomFields', ); - expect(alertWebhookPresentationSource).toContain( - 'export function getAlertWebhookTestLabel', - ); - expect(alertWebhookPresentationSource).toContain( - 'export function getAlertWebhookSubmitLabel', - ); + expect(alertWebhookPresentationSource).toContain('export function getAlertWebhookTestLabel'); + expect(alertWebhookPresentationSource).toContain('export function getAlertWebhookSubmitLabel'); }); });