diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index f7fedfb25..5c0620713 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -322,6 +322,12 @@ The legacy pricing handoff page may also route its visible redirect title and manual-link copy through the catalog, but `Pulse Account`, route paths, feature keys, query parameters, public URLs, and purchase-return state remain machine-stable and untranslated. +Customer-facing copy must use complete sentences or an intentional visual +separator such as a middle dot. Semicolons are reserved for machine-facing +syntax including commands, cookies, encoded data, CSS declarations, and HTML +entities. `frontend-modern/scripts/copy-style-audit.mjs` enforces that boundary +across production TypeScript and TSX, and every ordinary product-copy exception +must be rewritten instead of allowlisted. Alert thresholds consume the shared FilterBar primitive and route state, while the alerts subsystem owns the resource data and platform-specific threshold diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 7ebeecb03..9e4eba4fd 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,54 +1,141 @@ { "version": 1, - "base_sha": "ed5d983e8e028d43c35bbb45e6cae7a3fe3cf1bf", - "verified_at": "2026-08-18T11:20:00Z", + "base_sha": "fe9c64a4d377d677b9eafaae446b5b081b0d490e", + "verified_at": "2026-08-18T09:48:47Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/useAppRuntimeState.ts" + "frontend-modern/src/api/aiChatDevStreamFixture.ts", + "frontend-modern/src/components/AI/Chat/hooks/useChat.ts", + "frontend-modern/src/components/AI/Chat/index.tsx", + "frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", + "frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", + "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx", + "frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx", + "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx", + "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx", + "frontend-modern/src/components/Settings/NodeModalClusterMembersSection.tsx", + "frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx", + "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx", + "frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx", + "frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts", + "frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts", + "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx", + "frontend-modern/src/components/TokenRevealDialog.tsx", + "frontend-modern/src/components/UpdateBanner.tsx", + "frontend-modern/src/components/patrol/ApprovalSection.tsx", + "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts", + "frontend-modern/src/content/help/ai.ts", + "frontend-modern/src/features/actions/ActionDecisionPacket.tsx", + "frontend-modern/src/features/actions/ActionReviewDialog.tsx", + "frontend-modern/src/features/actions/aptActionPresentation.ts", + "frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx", + "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx", + "frontend-modern/src/features/docker/DockerIdentityConflictNotice.tsx", + "frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts", + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", + "frontend-modern/src/features/patrol/patrolControlPresentation.ts", + "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts", + "frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx", + "frontend-modern/src/features/standalone/agentMachineTableModel.ts", + "frontend-modern/src/i18n/index.ts", + "frontend-modern/src/i18n/messages.de.ts", + "frontend-modern/src/i18n/messages.es.ts", + "frontend-modern/src/i18n/messages.ts", + "frontend-modern/src/stores/websocket.ts", + "frontend-modern/src/utils/actionAuditPresentation.ts", + "frontend-modern/src/utils/aiFindingPresentation.ts", + "frontend-modern/src/utils/alertThresholdsPresentation.ts", + "frontend-modern/src/utils/apiTokenPresentation.ts", + "frontend-modern/src/utils/discoveryPresentation.ts", + "frontend-modern/src/utils/licensePresentation.ts", + "frontend-modern/src/utils/relayPresentation.ts", + "frontend-modern/src/utils/resourceAnalysisPresentation.ts" ], "content_sha256": { - "frontend-modern/src/useAppRuntimeState.ts": "eae9b60896cf6f8b816d77e8316afc72e47b9a44f7eed532dedf2def1b5aeeb6" + "frontend-modern/src/api/aiChatDevStreamFixture.ts": "73203e7794f2e732a7dc0b50df5b41e45e858df7a4126536acd4c93e47379511", + "frontend-modern/src/components/AI/Chat/hooks/useChat.ts": "ef7398972b813247af49861fb0918bb4539f043a6779543f79d5db761525c437", + "frontend-modern/src/components/AI/Chat/index.tsx": "3a896c83b64b4c3a9370e7fa979b9785232a8b09f2c403c917dd5ab52ae5da9c", + "frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts": "9d12767d795c038ae8cf5ef83d3d76a0390671003beb8058bbd584abdd96a0c9", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "69714072d2458787f4914127fadee1b2125181e9d44982d26c00a47c08f8f70d", + "frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx": "9c20711471dd2c95efe8e6030b6ca46bd1db2500219618f25e2caf71c50fcc18", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx": "87dbc433ac131af98331a4018a0b82f524053294076addb9a299c9f122a7db70", + "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx": "2db2c6470704743a1844c410403acc8132c6b43f7cdf2c061946ff4d0799b8f9", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx": "9748e2004e2efcbe76c29f56a5a07a8818ea967d64ac83e44e9ed7278e73cbca", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx": "14f6a2b385bb2c048726636a447c3e62e1410beb79d8b1e531873047ef1b8e3a", + "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx": "82000ab351789a6f9f7ffcae740abe7776d8842e68eede84135b2ec8543176e8", + "frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx": "da7130ee44a6b61a9447916465bec6548ca477b1c918e8ccdabf7b0565d13d25", + "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "5187bf82eebb5950fea336fd72b223b8d1f6ab97eeb418abc1bdf055b2c93d60", + "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "fbc03c3d9d13490fb190c3fcbc58020f553d8494cb55626a6d6639d2ecb73aa0", + "frontend-modern/src/components/Settings/NodeModalClusterMembersSection.tsx": "dbb553e75e4ee057c1d33b84356342581a7362ae1569742955fd0f3820668213", + "frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx": "7240466d76e6ef4b4225977f82fee5f0b20d7a34e5eb067a6b2e3d9973ea9383", + "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "edb75037be9e845505abed5088307fa170e6a62ba4a1ca57e5677c2d1a473635", + "frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx": "be737e03a7f6fbf89900c6821d4de5aff3613434856b3e55f22bb1154fadd617", + "frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts": "0bedc70ba984d1e894a9470761a86108685ebf19e026de19c9c7f290b6be635c", + "frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts": "0c062f5eedcc892d7bbfff65fd4bc7be9034b16813c2f4572e001878f511f867", + "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "b96a121c7c60546cb3d61b9b3d2dc6557f6f3d276eaa98c664aae8803bd0cc77", + "frontend-modern/src/components/TokenRevealDialog.tsx": "94b5fd3104f06d6adb3ce10c9466eacc6eac5a3a1df6b5fac83728a54afa57e0", + "frontend-modern/src/components/UpdateBanner.tsx": "b63e3a968baae1970025b2d4e2a16b1fa4982980307cd87945420d831fb45834", + "frontend-modern/src/components/patrol/ApprovalSection.tsx": "d16b92c67b42a74071d06c2a191007d06f721947c56e1718363d1fa2a63d13b8", + "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts": "9379c2672dc9fb79f44b6fbfd4d34871eef4f3c0effb26f022cbe73edb2d2cff", + "frontend-modern/src/content/help/ai.ts": "c00d01abff015784b40f38675df2b65510ced6ea0844ff7c86d529800b643283", + "frontend-modern/src/features/actions/ActionDecisionPacket.tsx": "4854edfefe43d0b32cd819e265008cee5e8a38fc79714f90c9695b713b094389", + "frontend-modern/src/features/actions/ActionReviewDialog.tsx": "a6b28e3731297ddfc6c4b837c7c5b58551d6fa18a38099dc86021a23729eccd0", + "frontend-modern/src/features/actions/aptActionPresentation.ts": "ebb1ba10b896746539866ceaea33db3f512329e2fc993d21cda34f89ee5b145f", + "frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx": "736ae64aa98b67f544c2487ff1d905e59e2cc565b3807f91c119f5241d34131f", + "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx": "4ec03d51b4ef7d1ed1b0a1e7513df1c77d9cf052659fb5a33efe438b4e4821b9", + "frontend-modern/src/features/docker/DockerIdentityConflictNotice.tsx": "3fc232aad32fe80944de39ae420d3b0e638c3880ee9a51ce5999ec4bfa712ccb", + "frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts": "2d3038e972213399c2440b09580b45efa464aef876e0742c4e302d4fbf8bd75b", + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "28f46c52ec8f8f888f21b71882e78870ccf93e35dbc9da8e1dafaa550ef634b9", + "frontend-modern/src/features/patrol/patrolControlPresentation.ts": "3cd991c6802a380bfca106bab0df166e6508166499d583d9d8ff85c2060ca80f", + "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts": "6c1a7b7d3878ac44ff3b364c4719166f4cadabf4b27f5161b236d3f4836a0acd", + "frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx": "3e124e0e058b84993c61f5f4b6430131d59f6c1909f4880e7f82602713455c02", + "frontend-modern/src/features/standalone/agentMachineTableModel.ts": "14b1c0f0b1050d7722a8787e8a1fbe88d6854bb83532d39db36f6a4d4d8df308", + "frontend-modern/src/i18n/index.ts": "5e2744946a866e2539fedad0b395db12a77661fb5fd94acb61879081a8e78a4e", + "frontend-modern/src/i18n/messages.de.ts": "25b282259570d69029bcabe58ba2ecfb1734ffce629696ec8aa10698df057fc2", + "frontend-modern/src/i18n/messages.es.ts": "ac86cdb7be83867dbca2b3bef63a445ccffaac1b3372504e9aa4cd3d0e8ffc5e", + "frontend-modern/src/i18n/messages.ts": "7dcb19a594e97b0b92c979625c14d36a8563c3933ab5209a39f3e6b43d7357eb", + "frontend-modern/src/stores/websocket.ts": "a9ebccaec56604cbab2e204c897ca91f5a2ae9675ad7fd0643b294e19fbede3d", + "frontend-modern/src/utils/actionAuditPresentation.ts": "01eb79cf96abc674579a671913b83354a62541e3fb35b3811ddb4dbeab8195f6", + "frontend-modern/src/utils/aiFindingPresentation.ts": "8d4a8bbba79c46552112a237c62402c2ef9c0f436b8a7cb6189c5911f1c3ec9a", + "frontend-modern/src/utils/alertThresholdsPresentation.ts": "1d3573df8d61ece176d34c8d815711f6a884dc588c8d201fcad93976a2598c92", + "frontend-modern/src/utils/apiTokenPresentation.ts": "782d45f1babb667107dea4cdaa8166778bf200adf50745b05a255babb6d52521", + "frontend-modern/src/utils/discoveryPresentation.ts": "d65a43bfbfd3433425064f02df58656756828a309f55da1ca0c8237a1f17c04f", + "frontend-modern/src/utils/licensePresentation.ts": "cfa268d06f8364c0aef59ce8346ef4a0fbe0c164932a3276038e35d96f715206", + "frontend-modern/src/utils/relayPresentation.ts": "354228848ebe2943cbf0db89dba6e5cfcabb1cb2f7984b7a7dfd1dd2857588e6", + "frontend-modern/src/utils/resourceAnalysisPresentation.ts": "5a3254abf8c2613f595e27f38aa60ad9003c2c7b7c5c3c684ee26038c9f4ac6d" }, - "routes": [ - "/login", - "/", - "/proxmox/overview", - "/docker/overview", - "/kubernetes/overview", - "/truenas/overview", - "/vsphere/overview", - "/settings/infrastructure" - ], + "routes": ["/patrol", "/actions", "/settings/pulse-intelligence/patrol"], "viewports": [ { - "width": 1440, + "width": 1280, "height": 900 }, { "width": 390, "height": 844 + }, + { + "width": 320, + "height": 568 } ], "states": [ - "signed-out login form", - "authenticated shell on a populated estate, all six platform tabs present", - "Docker, Kubernetes, TrueNAS and vSphere platform pages populated", - "authenticated shell on an estate with zero resources", - "empty estate landing on /settings/infrastructure with no platform tabs in the navigation landmark", - "populated and empty estates both at 390x844" + "Live Patrol Inbox with ranked decisions and governed approval handoff", + "Patrol Protection workspace", + "Patrol Activity workspace with Actions and findings history handoffs", + "Actions open queue", + "Patrol settings after asynchronous shell load", + "Canonical mobile product routes at compact and modern phone widths" ], "interactions": [ - "signed in against a backend built from this change and confirmed the shell issues one /api/state/summary probe and zero /api/state requests", - "confirmed navigation resolved from the admission facet with all six platform tabs and landed on /proxmox/overview", - "switched to the Docker tab and confirmed 80 rows", - "switched to the Kubernetes tab and confirmed 180 rows", - "switched to the TrueNAS tab and confirmed 3 rows", - "switched to the vSphere tab and confirmed 32 rows", - "returned to the Proxmox tab and confirmed 79 rows", - "signed in against a second backend with an empty estate and confirmed the navigation landmark contains no platform tabs at all, rather than a stale or hidden set", - "confirmed the empty estate routes to /settings/infrastructure, which is the no-platform destination", - "confirmed the empty estate also issues zero /api/state requests", - "resized both estates to 390x844 and confirmed the populated one still renders 79 rows and the empty one renders none", - "confirmed zero console or page errors on both estates" + "Used the live in-app Browser to inspect Patrol, Actions, and Patrol settings and confirmed zero rendered semicolon punctuation on each surface", + "Switched Patrol between Inbox, Protection, and Activity and confirmed each rendered workspace remained semicolon-free", + "Confirmed Patrol remains a primary System navigation tab while Actions remains the subordinate review route linked from Patrol", + "Passed the mobile Chrome canonical-route overflow sweep at 320x568 and 390x844, including Patrol" ] -} \ No newline at end of file +} diff --git a/frontend-modern/package.json b/frontend-modern/package.json index 7d4f33082..c657ae16e 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -35,9 +35,10 @@ "test:coverage": "vitest run --coverage --coverage.provider=v8 --coverage.include=src/**/*.ts --coverage.include=src/**/*.tsx --coverage.exclude=src/index.tsx", "test:coverage:ai": "vitest run --coverage --coverage.provider=v8 --coverage.thresholds.100 --coverage.thresholds.perFile --coverage.include=src/components/AI/aiChatUtils.ts", "type-check": "tsc --noEmit", - "lint": "npm run lint:eslint && npm run lint:theme && npm run lint:canonical-platforms", + "lint": "npm run lint:eslint && npm run lint:theme && npm run lint:copy && npm run lint:canonical-platforms", "lint:canonical-platforms": "node scripts/canonical-platform-audit.mjs", "lint:eslint": "eslint \"src/**/*.{ts,tsx}\"", + "lint:copy": "node scripts/copy-style-audit.mjs", "lint:theme": "node scripts/theme-audit.mjs", "lint:headers": "node scripts/header-audit.mjs", "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", diff --git a/frontend-modern/scripts/copy-style-audit.mjs b/frontend-modern/scripts/copy-style-audit.mjs new file mode 100644 index 000000000..c19d90cd8 --- /dev/null +++ b/frontend-modern/scripts/copy-style-audit.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import ts from 'typescript'; + +const ROOT = process.cwd(); +const SRC_DIR = path.join(ROOT, 'src'); +const TEMPLATE_KINDS = new Set([ + ts.SyntaxKind.TemplateHead, + ts.SyntaxKind.TemplateMiddle, + ts.SyntaxKind.TemplateTail, +]); +// This module's entire output contract is executable shell, PowerShell, or C# +// bootstrap syntax. It does not own customer-facing prose. +const MACHINE_SYNTAX_ONLY_FILES = new Set(['src/utils/agentInstallCommand.ts']); +const TECHNICAL_DELIMITER_FILES = new Set([ + 'src/components/Settings/useBackupTransferFlow.ts', + 'src/utils/resourcePolicyPresentation.ts', +]); +const findings = []; + +function toRelative(absPath) { + return path.relative(ROOT, absPath).replaceAll(path.sep, '/'); +} + +function collectSourceFiles(directory) { + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.name === '__tests__' || entry.name === 'node_modules') continue; + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...collectSourceFiles(fullPath)); + continue; + } + if (!entry.isFile() || !/\.(ts|tsx)$/.test(entry.name) || entry.name.includes('.test.')) { + continue; + } + files.push(fullPath); + } + return files; +} + +function removeHtmlEntities(value) { + return value.replace(/&(?:#\d+|#x[\da-f]+|[a-z][\da-z]+);/gi, ''); +} + +function isTechnicalSemicolon(relativePath, value) { + if (MACHINE_SYNTAX_ONLY_FILES.has(relativePath)) return true; + if (TECHNICAL_DELIMITER_FILES.has(relativePath) && /^;(?: Secure)?\s*$/.test(value)) { + return true; + } + if (relativePath === 'src/components/Settings/nodeModalModel.ts' && value.includes('pveum ')) { + return true; + } + if ( + relativePath === 'src/components/Settings/useInfrastructureOperationsState.tsx' && + (/\$env:|\$\(|\b(?:bash|sudo|else|exit|fi)\b/.test(value) || /^;\s*$/.test(value)) + ) { + return true; + } + if ( + relativePath === 'src/components/shared/useSearchTipsPopoverState.ts' && + /px;(?:top|width|max-height):/.test(value) + ) { + return true; + } + if (relativePath === 'src/utils/apiClient.ts' && /^(?:;|; Secure|=; Path=|; Path=)/.test(value)) { + return true; + } + return ( + /(?:^|\s)animation-(?:delay|duration):/.test(value) || + /(?:^|["'])position:fixed !important;/.test(value) || + /charset=utf-8/i.test(value) || + /;base64,/i.test(value) || + /<[^>]+style=["'][^"']*;/.test(value) + ); +} + +function inspectFile(absPath) { + const relativePath = toRelative(absPath); + const source = fs.readFileSync(absPath, 'utf8'); + const sourceFile = ts.createSourceFile( + relativePath, + source, + ts.ScriptTarget.Latest, + true, + relativePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + const visit = (node) => { + let value; + if ( + ts.isStringLiteralLike(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + ts.isJsxText(node) || + TEMPLATE_KINDS.has(node.kind) + ) { + value = node.text; + } + + if (value) { + const productText = removeHtmlEntities(value); + if (productText.includes(';') && !isTechnicalSemicolon(relativePath, productText)) { + const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + findings.push({ + file: relativePath, + line: position.line + 1, + preview: productText.trim().replace(/\s+/g, ' ').slice(0, 120), + }); + } + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); +} + +for (const file of collectSourceFiles(SRC_DIR)) inspectFile(file); + +if (findings.length > 0) { + console.error( + 'Copy style audit failed. Replace semicolons in user-visible copy with sentences or a visual separator.', + ); + for (const finding of findings) { + console.error(`- ${finding.file}:${finding.line}: ${finding.preview}`); + } + process.exit(1); +} + +console.log('Copy style audit passed with no user-visible semicolons.'); diff --git a/frontend-modern/src/api/__tests__/aiChat.test.ts b/frontend-modern/src/api/__tests__/aiChat.test.ts index 1bcc84fc7..bf081bb63 100644 --- a/frontend-modern/src/api/__tests__/aiChat.test.ts +++ b/frontend-modern/src/api/__tests__/aiChat.test.ts @@ -980,7 +980,7 @@ describe('AIChatAPI', () => { type: 'workflow_state', data: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', provider: 'openrouter', model: 'openrouter:deepseek/deepseek-chat', attempt: 2, @@ -1029,7 +1029,7 @@ describe('AIChatAPI', () => { type: 'workflow_state', data: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }, }); expect(onEvent.mock.calls[5][0]).toMatchObject({ diff --git a/frontend-modern/src/api/__tests__/aiChatEvents.test.ts b/frontend-modern/src/api/__tests__/aiChatEvents.test.ts index 4fa6f1703..04a8b7f30 100644 --- a/frontend-modern/src/api/__tests__/aiChatEvents.test.ts +++ b/frontend-modern/src/api/__tests__/aiChatEvents.test.ts @@ -52,7 +52,7 @@ describe('AI chat stream event contract', () => { it('exposes provider retry metadata on workflow state events', () => { const workflow: WorkflowStateData = { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', state: 'investigating', attempt: 2, max_attempts: 2, diff --git a/frontend-modern/src/api/aiChatDevStreamFixture.ts b/frontend-modern/src/api/aiChatDevStreamFixture.ts index bfbff05f7..8ad55828d 100644 --- a/frontend-modern/src/api/aiChatDevStreamFixture.ts +++ b/frontend-modern/src/api/aiChatDevStreamFixture.ts @@ -909,7 +909,7 @@ const buildProviderRetryFixtureEvents = (model?: string): AIChatStreamEvent[] => type: 'workflow_state', data: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', provider: selectedProvider, model: selectedModel, attempt: 2, @@ -960,7 +960,7 @@ const buildStreamIdleFixtureEvents = (model?: string): AIChatStreamEvent[] => [ type: 'workflow_state', data: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }, }, { diff --git a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx index 5ec67d000..186dc1eec 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx @@ -826,7 +826,7 @@ describe('AIChat', () => { expect(transcriptValue).toContain('fallback transcript body'); expect(document.activeElement).toBe(transcriptField); expect(mockNotificationStore.warning).toHaveBeenCalledWith( - 'Clipboard blocked; transcript opened for manual copy', + 'Clipboard blocked. Transcript opened for manual copy', 4000, ); @@ -5338,7 +5338,7 @@ describe('AIChat', () => { isStreaming: true, workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', provider: 'openrouter', model: 'openrouter:qwen/qwen3.7-plus', }, @@ -5352,7 +5352,7 @@ describe('AIChat', () => { type: 'workflow_status', workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', provider: 'openrouter', model: 'openrouter:qwen/qwen3.7-plus', }, @@ -6077,7 +6077,7 @@ describe('AIChat', () => { streamEvents: [], workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, @@ -6088,7 +6088,7 @@ describe('AIChat', () => { renderChat(); expect(screen.getByLabelText('Assistant active turn status')).toHaveTextContent( - 'Selected route connection failed before any output; retrying. · attempt 2/3 · retrying in 1.9s', + 'Selected route connection failed before any output. Retrying. · attempt 2/3 · retrying in 1.9s', ); expect( screen diff --git a/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx index 1738e9ca4..0794566b4 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx @@ -750,7 +750,7 @@ describe('MessageItem', () => { type: 'workflow_status', workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 1200, @@ -759,7 +759,7 @@ describe('MessageItem', () => { ], workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 1200, @@ -786,7 +786,7 @@ describe('MessageItem', () => { type: 'workflow_status', workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, @@ -796,7 +796,7 @@ describe('MessageItem', () => { ], workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, @@ -867,7 +867,7 @@ describe('MessageItem', () => { }, { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, diff --git a/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.branchcov.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.branchcov.test.ts index c4705645a..7f58c8b3c 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.branchcov.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.branchcov.test.ts @@ -525,7 +525,7 @@ describe('isFresherStatusCandidate branch coverage', () => { isStreaming: true, workflowStatus: { phase: 'stream_idle', - message: 'Still working; waiting for more response data.', + message: 'Still working. Waiting for more response data.', startedAt: 2_000, }, streamEvents: [ @@ -533,7 +533,7 @@ describe('isFresherStatusCandidate branch coverage', () => { type: 'workflow_status', workflowStatus: { phase: 'stream_idle', - message: 'Still working; waiting for more response data.', + message: 'Still working. Waiting for more response data.', startedAt: 2_000, }, startedAt: 2_000, @@ -546,7 +546,7 @@ describe('isFresherStatusCandidate branch coverage', () => { ), ).toEqual({ type: 'thinking', - text: 'Still working; waiting for more response data.', + text: 'Still working. Waiting for more response data.', startedAt: 2_000, }); }); diff --git a/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.test.ts index bc90eabd0..00fbd563b 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/activeTurnStatus.test.ts @@ -254,7 +254,7 @@ describe('getAssistantActiveTurnStatus', () => { type: 'workflow_status', workflowStatus: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', startedAt: 2_000, }, startedAt: 2_000, @@ -267,7 +267,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'thinking', - text: 'Assistant is still working; waiting for the next stream event.', + text: 'Assistant is still working. Waiting for the next stream event.', startedAt: 2_000, }); }); @@ -328,7 +328,7 @@ describe('getAssistantActiveTurnStatus', () => { }, { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, @@ -358,7 +358,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'retrying', - text: 'Selected route connection failed before any output; retrying. · attempt 2/3 · retrying in 3.2s', + text: 'Selected route connection failed before any output. Retrying. · attempt 2/3 · retrying in 3.2s', startedAt: 1_200, }); }); @@ -366,7 +366,7 @@ describe('getAssistantActiveTurnStatus', () => { it('keeps current provider retry liveness ahead of later generic content status', () => { const retryStatus = { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 3, retryAfterMs: 3200, @@ -400,7 +400,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'retrying', - text: 'Selected route connection failed before any output; retrying. · attempt 2/3 · retrying in 2.1s', + text: 'Selected route connection failed before any output. Retrying. · attempt 2/3 · retrying in 2.1s', startedAt: 1_200, }); }); @@ -419,7 +419,7 @@ describe('getAssistantActiveTurnStatus', () => { }, { phase: 'stream_idle', - message: 'OpenRouter is still working; waiting for more response data.', + message: 'OpenRouter is still working. Waiting for more response data.', startedAt: 1_200, }, ]; @@ -446,7 +446,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'thinking', - text: 'OpenRouter is still working; waiting for more response data.', + text: 'OpenRouter is still working. Waiting for more response data.', startedAt: 1_200, }); }); @@ -454,7 +454,7 @@ describe('getAssistantActiveTurnStatus', () => { it('keeps current stream-idle liveness ahead of later generic content status', () => { const idleStatus = { phase: 'stream_idle', - message: 'OpenRouter is still working; waiting for more response data.', + message: 'OpenRouter is still working. Waiting for more response data.', startedAt: 1_200, }; @@ -485,7 +485,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'thinking', - text: 'OpenRouter is still working; waiting for more response data.', + text: 'OpenRouter is still working. Waiting for more response data.', startedAt: 1_200, }); }); @@ -540,7 +540,7 @@ describe('getAssistantActiveTurnStatus', () => { assistantMessage({ workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 2, retryAfterMs: 200, @@ -552,7 +552,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'retrying', - text: 'Selected route connection failed before any output; retrying. · attempt 2/2 · retrying in 200ms', + text: 'Selected route connection failed before any output. Retrying. · attempt 2/2 · retrying in 200ms', startedAt, }); }); @@ -565,7 +565,7 @@ describe('getAssistantActiveTurnStatus', () => { assistantMessage({ workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 2, retryAfterMs: 3200, @@ -578,7 +578,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'retrying', - text: 'Selected route connection failed before any output; retrying. · attempt 2/2 · retrying in 1.9s', + text: 'Selected route connection failed before any output. Retrying. · attempt 2/2 · retrying in 1.9s', startedAt, }); }); @@ -591,7 +591,7 @@ describe('getAssistantActiveTurnStatus', () => { assistantMessage({ workflowStatus: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 2, retryAfterMs: 3200, @@ -604,7 +604,7 @@ describe('getAssistantActiveTurnStatus', () => { ), ).toEqual({ type: 'retrying', - text: 'Selected route connection failed before any output; retrying. · attempt 2/2 · retrying now', + text: 'Selected route connection failed before any output. Retrying. · attempt 2/2 · retrying now', startedAt, }); }); diff --git a/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts index badce9556..13e1ec523 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts @@ -1642,7 +1642,7 @@ describe('useChat', () => { type: 'workflow_state', data: { phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, max_attempts: 2, retry_after_ms: 200, @@ -1653,7 +1653,7 @@ describe('useChat', () => { expect(assistant.workflowStatus).toEqual( expect.objectContaining({ phase: 'provider_retry', - message: 'Selected route connection failed before any output; retrying.', + message: 'Selected route connection failed before any output. Retrying.', attempt: 2, maxAttempts: 2, retryAfterMs: 200, @@ -1825,7 +1825,7 @@ describe('useChat', () => { type: 'workflow_state', data: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }, }); @@ -1833,7 +1833,7 @@ describe('useChat', () => { expect(assistant.workflowStatus).toEqual( expect.objectContaining({ phase: 'stream_idle', - message: 'OpenRouter is still working; waiting for more response data.', + message: 'OpenRouter is still working. Waiting for more response data.', provider: 'openrouter', model: 'openrouter:qwen/qwen3.7-plus', }), @@ -1850,7 +1850,7 @@ describe('useChat', () => { type: 'workflow_status', workflowStatus: expect.objectContaining({ phase: 'stream_idle', - message: 'OpenRouter is still working; waiting for more response data.', + message: 'OpenRouter is still working. Waiting for more response data.', provider: 'openrouter', model: 'openrouter:qwen/qwen3.7-plus', }), @@ -1876,7 +1876,7 @@ describe('useChat', () => { type: 'workflow_state', data: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }, }); @@ -1884,7 +1884,7 @@ describe('useChat', () => { expect(assistant.workflowStatus).toEqual( expect.objectContaining({ phase: 'stream_idle', - message: 'OpenRouter is still working; waiting for more response data.', + message: 'OpenRouter is still working. Waiting for more response data.', provider: 'openrouter', model: 'openrouter:qwen/qwen3.7-plus', }), @@ -2068,7 +2068,7 @@ describe('useChat', () => { type: 'workflow_state', data: { phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }, }); @@ -2076,7 +2076,7 @@ describe('useChat', () => { expect(assistant.workflowStatus).toEqual( expect.objectContaining({ phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }), ); expect(assistant.streamEvents).toEqual([ @@ -2084,7 +2084,7 @@ describe('useChat', () => { type: 'workflow_status', workflowStatus: expect.objectContaining({ phase: 'stream_idle', - message: 'Assistant is still working; waiting for the next stream event.', + message: 'Assistant is still working. Waiting for the next stream event.', }), }), ]); diff --git a/frontend-modern/src/components/AI/Chat/hooks/useChat.ts b/frontend-modern/src/components/AI/Chat/hooks/useChat.ts index 41507034e..f49d8f4b3 100644 --- a/frontend-modern/src/components/AI/Chat/hooks/useChat.ts +++ b/frontend-modern/src/components/AI/Chat/hooks/useChat.ts @@ -255,7 +255,7 @@ export function useChat(options: UseChatOptions = {}) { message?.workflowStatus?.model?.trim() || message?.model?.trim() || undefined, - message: `${getAIProviderDisplayName(provider)} is still working; waiting for more response data.`, + message: `${getAIProviderDisplayName(provider)} is still working. Waiting for more response data.`, }; }; @@ -1977,7 +1977,7 @@ export function useChat(options: UseChatOptions = {}) { clientMessageId: entry.messageId, }); if (!result.accepted) { - logger.debug('[useChat] Steer not accepted; follow-up stays queued', { + logger.debug('[useChat] Steer not accepted. Follow-up stays queued', { reason: result.reason, }); return; @@ -1988,7 +1988,7 @@ export function useChat(options: UseChatOptions = {}) { ), ); } catch (error) { - logger.warn('[useChat] Steering attempt failed; follow-up stays queued', error); + logger.warn('[useChat] Steering attempt failed. Follow-up stays queued', error); } }; diff --git a/frontend-modern/src/components/AI/Chat/index.tsx b/frontend-modern/src/components/AI/Chat/index.tsx index a821b3f81..f37906410 100644 --- a/frontend-modern/src/components/AI/Chat/index.tsx +++ b/frontend-modern/src/components/AI/Chat/index.tsx @@ -656,7 +656,7 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde : `Last known state: ${statusLabel}` : undefined, safetyNote: isPatrolRun - ? 'Patrol run context is review-only; actions still require governed approval.' + ? 'Patrol run context is review-only. Actions still require governed approval.' : actionCount > 0 ? 'Detailed command payloads stay in governed approval context.' : undefined, @@ -1919,7 +1919,7 @@ export const AIChat: Component = (props) => { return; } setTranscriptCopyFallback({ generatedAt, transcript }); - notificationStore.warning('Clipboard blocked; transcript opened for manual copy', 4000); + notificationStore.warning('Clipboard blocked. Transcript opened for manual copy', 4000); }; const copyLastAssistantAnswer = async () => { diff --git a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx index bc0622614..c3c51e5c5 100644 --- a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx +++ b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx @@ -330,7 +330,7 @@ describe('InvestigateAlertButton', () => { subject: 'Warning cpu on test-vm', statusLabel: expect.stringContaining('Warning alert'), detailLines: expect.arrayContaining([ - 'Current value 82.5%; threshold 80.0%', + 'Current value 82.5% · threshold 80.0%', 'Node: PVE Node 1', 'Message: CPU usage is high', ]), @@ -394,7 +394,7 @@ describe('InvestigateAlertButton', () => { expect(briefing.statusLabel).toContain('Warnung-Warnmeldung'); expect(briefing.detailLines).toEqual( expect.arrayContaining([ - 'Aktueller Wert 82.5%; Schwellwert 80.0%', + 'Aktueller Wert 82.5% · Schwellwert 80.0%', 'Knoten: PVE Node 1', 'Meldung: CPU usage is high', ]), diff --git a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.branchcov0712.test.ts b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.branchcov0712.test.ts index 9e2d197de..b72d2c2e9 100644 --- a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.branchcov0712.test.ts +++ b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.branchcov0712.test.ts @@ -182,7 +182,7 @@ describe('formatContextLine (exercised via buildAlertAssistantHandoff)', () => { // Briefing's message ternary (alert.message ? ...) also drops the line; with a // metric cpu alert only currentMetric + node detailLines remain. expect(handoff.context.briefing.detailLines).toEqual([ - 'Current value 82.5%; threshold 80.0%', + 'Current value 82.5% · threshold 80.0%', 'Node: PVE Node 1', ]); }); @@ -191,7 +191,7 @@ describe('formatContextLine (exercised via buildAlertAssistantHandoff)', () => { const handoff = buildHandoff({ message: undefined as unknown as string }); expect(handoff.context.handoffContext).not.toContain('Message:'); expect(handoff.context.briefing.detailLines).toEqual([ - 'Current value 82.5%; threshold 80.0%', + 'Current value 82.5% · threshold 80.0%', 'Node: PVE Node 1', ]); }); @@ -200,7 +200,7 @@ describe('formatContextLine (exercised via buildAlertAssistantHandoff)', () => { const handoff = buildHandoff({ message: '' }); expect(handoff.context.handoffContext).not.toContain('Message:'); expect(handoff.context.briefing.detailLines).toEqual([ - 'Current value 82.5%; threshold 80.0%', + 'Current value 82.5% · threshold 80.0%', 'Node: PVE Node 1', ]); }); @@ -285,7 +285,7 @@ describe('buildAlertAssistantHandoff (node-label, metadata-chain & briefing tern it('keeps the currentMetric detailLine for a metric alert (hasMetricValues true -> briefing ternary true arm)', () => { const handoff = buildHandoff({ type: 'cpu', value: 92.5, threshold: 80 }); - expect(handoff.context.briefing.detailLines).toContain('Current value 92.5%; threshold 80.0%'); + expect(handoff.context.briefing.detailLines).toContain('Current value 92.5% · threshold 80.0%'); expect(handoff.context.handoffContext).toContain('Current Value: 92.5%'); expect(handoff.context.handoffContext).toContain('Threshold: 80.0%'); }); diff --git a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts index c808446c4..3d0e53292 100644 --- a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts +++ b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts @@ -48,7 +48,7 @@ describe('alertAssistantHandoffModel', () => { subject: 'Warning cpu on app-vm', statusLabel: 'Warning alert · Active 5 mins', detailLines: [ - 'Current value 82.5%; threshold 80.0%', + 'Current value 82.5% · threshold 80.0%', 'Node: PVE Node 1', 'Message: CPU usage is high', ], @@ -121,6 +121,6 @@ describe('alertAssistantHandoffModel', () => { }); const briefing = handoff.context.briefing as { detailLines: string[] }; - expect(briefing.detailLines).toContain('Current value 92.5%; threshold 80.0%'); + expect(briefing.detailLines).toContain('Current value 92.5% · threshold 80.0%'); }); }); diff --git a/frontend-modern/src/components/Alerts/__tests__/incidentAssistantHandoffModel.branchcov2.test.ts b/frontend-modern/src/components/Alerts/__tests__/incidentAssistantHandoffModel.branchcov2.test.ts index 1122f644b..e3f1b0e70 100644 --- a/frontend-modern/src/components/Alerts/__tests__/incidentAssistantHandoffModel.branchcov2.test.ts +++ b/frontend-modern/src/components/Alerts/__tests__/incidentAssistantHandoffModel.branchcov2.test.ts @@ -492,7 +492,7 @@ describe('buildAlertIncidentAssistantHandoff (resource-label, event-count & capp it('renders the fixed boundary footer lines in handoffContext', () => { const handoff = buildHandoff(makeIncident()); expect(handoff.context.handoffContext).toContain( - 'Timeline Boundary: Command events are summarized only; raw command details and output stay in the incident or governed approval surface.', + 'Timeline Boundary: Command events are summarized only. Raw command details and output stay in the incident or governed approval surface.', ); expect(handoff.context.handoffContext).toContain( 'Operator Boundary: This incident handoff is model-only context for explanation and review. Diagnostics, remediation, and any command execution require explicit operator approval.', diff --git a/frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts b/frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts index 0dc976850..40387e999 100644 --- a/frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts +++ b/frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts @@ -150,7 +150,7 @@ function buildIncidentAssistantModelContext({ formatContextLine('Timeline Summary', eventCountLabel), formatContextLine('Message', incident.message), ...eventLines, - 'Timeline Boundary: Command events are summarized only; raw command details and output stay in the incident or governed approval surface.', + 'Timeline Boundary: Command events are summarized only. Raw command details and output stay in the incident or governed approval surface.', 'Operator Boundary: This incident handoff is model-only context for explanation and review. Diagnostics, remediation, and any command execution require explicit operator approval.', ] .filter((line): line is string => Boolean(line)) diff --git a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx index 933a3372e..130384f4e 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx @@ -550,7 +550,7 @@ export const ResourceOperatorStateSection: Component setNote(e.currentTarget.value)} - placeholder="e.g. Production database; page before rebooting" + placeholder="e.g. Production database. Page before rebooting" class="mt-1 block min-h-16 w-full resize-y text-xs rounded border border-border bg-surface px-2 py-1.5 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 disabled:opacity-50" disabled={saving()} maxLength={500} diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx index 216ce15c1..3025c812a 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx @@ -97,7 +97,7 @@ const getThermalPressurePresentation = (resource: Resource): ThermalPressurePres .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key.replace(/_/g, ' ')} ${Math.round(value)}%`); const source = thermalState.source ? ` via ${thermalState.source}` : ''; - const limitSummary = limits.length > 0 ? `; limits: ${limits.join(', ')}` : ''; + const limitSummary = limits.length > 0 ? ` · limits: ${limits.join(', ')}` : ''; return { label, diff --git a/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx b/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx index f3aedecff..984e89741 100644 --- a/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx +++ b/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx @@ -99,7 +99,7 @@ export const patrolReadinessBannerHeadline = ( if (result.max_verified_mode === 'approval') return 'Verified for Watch only and Ask first'; if (result.max_verified_mode === 'monitor') return 'Verified for Watch only'; if (result.transport_healthy && !result.patrol_capable) - return 'Provider connected; Patrol capability not verified'; + return 'Provider connected. Patrol capability not verified'; if (result.status === 'warning') return 'Patrol model needs attention'; return 'Patrol model not verified'; }; diff --git a/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx index 577765828..7c49e4d08 100644 --- a/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx +++ b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx @@ -457,7 +457,7 @@ export const AgentIntegrationsPanel: Component = () => {

These values come from{' '} /api/agent/capabilities. Use them to - build clients against this instance; they do not change Patrol mode. + build clients against this instance. They do not change Patrol mode.

0}>
@@ -528,7 +528,7 @@ export const AgentIntegrationsPanel: Component = () => { )} . Use narrower tokens when the client should only perform part of the - allowed work; each capability below shows its required scope. + allowed work. Each capability below shows its required scope.

0}> diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx index fe63a98fd..ca4ae163d 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx @@ -653,7 +653,7 @@ export const AvailabilityTargetSlot: Component = (p padding="sm" description={ result().outcome === 'indeterminate' - ? `No UDP rejection was received in ${result().latencyMillis} ms; the port is open or filtered, not proven reachable.` + ? `No UDP rejection was received in ${result().latencyMillis} ms. The port is open or filtered, not proven reachable.` : result().success ? availabilityTestSuccessDescription(result()) : result().error || 'Probe failed.' diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx index d09fd61c0..57b07f5ad 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx @@ -31,7 +31,7 @@ export interface TrueNASCredentialSlotProps { const describeTransport = (transport: NonNullable): string => { let description: string; if (transport.mode === 'jsonrpc-websocket') { - description = `Transport: JSON-RPC WebSocket${transport.tls ? ' over TLS' : ''}; authentication: ${transport.authMechanism || 'session'}.`; + description = `Transport: JSON-RPC WebSocket${transport.tls ? ' over TLS' : ''} · authentication: ${transport.authMechanism || 'session'}.`; } else if (transport.mode === 'legacy-rest') { description = `Transport: legacy REST (${transport.applianceVersion || 'legacy TrueNAS'}). ${transport.legacyReason || ''}`.trim(); @@ -340,7 +340,7 @@ export const TrueNASCredentialSlot: Component = (pro
- Removing forgets this connection from Pulse; credentials on the platform itself are + Removing forgets this connection from Pulse. Credentials on the platform itself are untouched.
diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx index ee7307a3b..9f35acc9b 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx @@ -255,7 +255,7 @@ export const VMwareCredentialSlot: Component = (props
- Removing forgets this connection from Pulse; credentials on the platform itself are + Removing forgets this connection from Pulse. Credentials on the platform itself are untouched.
diff --git a/frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx b/frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx index c94b80dd3..a1b565e47 100644 --- a/frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx +++ b/frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx @@ -191,7 +191,7 @@ export const DiagnosticsResultsPanel: Component = > {(probe) => (
- Monitor: {pbs.state || (pbs.connected ? 'active' : 'unreachable')}; live + Monitor: {pbs.state || (pbs.connected ? 'active' : 'unreachable')}. Live check: {probe().connected ? 'connected' : 'failed'}
)} diff --git a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx index d572d732c..d75ce6004 100644 --- a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx @@ -314,7 +314,7 @@ export const InfrastructureAgentDoctorPage: Component {tokenGatedTargetCount() === 1 ? 'One repair needs a fresh scoped agent credential before Pulse can show its command.' - : `${tokenGatedTargetCount()} repairs need separate scoped credentials. Each credential binds to the first agent that uses it; select one installation, generate and run its command, then continue with the next.`} + : `${tokenGatedTargetCount()} repairs need separate scoped credentials. Each credential binds to the first agent that uses it. Select one installation, generate and run its command, then continue with the next.`}

diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx index d53de0402..47d4727a3 100644 --- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx @@ -290,7 +290,7 @@ export const InfrastructureInstallerSection: ComponentProxmox VE node and enable{' '} Pulse command execution. Then start Pulse with{' '} - PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true; optionally restrict it + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true. Optionally restrict it with PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS=101,102. Pulse uses bounded pct exec inventory for docker ps and{' '} docker stats, skips guests that already have their own agent, and does @@ -342,7 +342,7 @@ export const InfrastructureInstallerSection: Component Revoke the token any time from{' '} - Settings → Infrastructure → Tokens; the agent + Settings → Infrastructure → Tokens. The agent stops reporting immediately. @@ -586,7 +586,7 @@ export const InfrastructureInstallerSection: Component state.setInsecureMode(event.currentTarget.checked)} class="rounded text-blue-600 focus:ring-blue-500" /> - Skip TLS certificate verification (self-signed certs; not recommended) + Skip TLS certificate verification (self-signed certs, not recommended)
diff --git a/frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx b/frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx index 2fc65c286..0ef1c10f7 100644 --- a/frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx +++ b/frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx @@ -157,7 +157,7 @@ export const NodeModalMonitoringSection: Component

Monitor physical disk health (SMART)

- This will spin up idle HDDs; leave disabled if you rely on drive standby. + This will spin up idle HDDs. Leave disabled if you rely on drive standby.

Docker inside Proxmox LXCs: use this host-agent path instead of installing Pulse Agent in every guest. The copied command enables Pulse - command execution on the Proxmox node; the Pulse server still must be opted in + command execution on the Proxmox node. The Pulse server still must be opted in with{' '} PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true @@ -283,7 +283,7 @@ export const NodeModalSetupGuideSection: Component = (pro
- Removing forgets this connection from Pulse; credentials on the platform itself are + Removing forgets this connection from Pulse. Credentials on the platform itself are untouched.
diff --git a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx index 33ca9345a..3f93f772d 100644 --- a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx @@ -1286,7 +1286,7 @@ describe('AISettings provider save failure context', () => { fireEvent.click(await screen.findByRole('button', { name: 'Check Patrol model' })); expect( - await screen.findByText('Provider connected; Patrol capability not verified'), + await screen.findByText('Provider connected. Patrol capability not verified'), ).toBeInTheDocument(); expect(screen.getByText('Evaluation detail')).toBeInTheDocument(); expect(screen.getByText(/expected exactly one tool call, got 0/)).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/DiagnosticsResultsPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/DiagnosticsResultsPanel.test.tsx index 405d90a21..8ed287ae9 100644 --- a/frontend-modern/src/components/Settings/__tests__/DiagnosticsResultsPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/DiagnosticsResultsPanel.test.tsx @@ -292,6 +292,6 @@ describe('DiagnosticsResultsPanel', () => { )); expect(screen.getByText('pbs-primary')).toBeInTheDocument(); - expect(screen.getByText('Monitor: unreachable; live check: connected')).toBeInTheDocument(); + expect(screen.getByText('Monitor: unreachable. Live check: connected')).toBeInTheDocument(); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/infrastructureAgentDoctorModel.test.ts b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentDoctorModel.test.ts index b5d8d6008..259188f5c 100644 --- a/frontend-modern/src/components/Settings/__tests__/infrastructureAgentDoctorModel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentDoctorModel.test.ts @@ -743,7 +743,7 @@ describe('Agent Doctor model', () => { }), ]); - expect(report).toContain('Pulse Agent Doctor report (2 agents; 1 critical, 1 healthy)'); + expect(report).toContain('Pulse Agent Doctor report (2 agents · 1 critical, 1 healthy)'); expect(report).toContain('host-1 (Machine)'); expect(report).toContain(' Connection agent:host-1'); expect(report).toContain(' Status Critical'); @@ -785,7 +785,7 @@ describe('Agent Doctor model', () => { }), ]); - expect(report).toContain('Pulse Agent Doctor report (1 agent; 1 critical)'); + expect(report).toContain('Pulse Agent Doctor report (1 agent · 1 critical)'); expect(report).not.toContain('Reported agent'); expect(report).not.toContain('Supported target'); expect(report).not.toContain('Last seen'); diff --git a/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0720pm.test.ts b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0720pm.test.ts index 89f663ce7..5837fa3c9 100644 --- a/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0720pm.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0720pm.test.ts @@ -1043,8 +1043,8 @@ describe('collectInfrastructureAgentDoctorTargets (updaterPresentation arms)', ( expect(updaterFor('update-available', true)).toBe('Update queued automatically'); }); - it('labels update-available + manual as "Update available; manual action required"', () => { - expect(updaterFor('update-available', false)).toBe('Update available; manual action required'); + it('labels update-available + manual as "Update available. Manual action required"', () => { + expect(updaterFor('update-available', false)).toBe('Update available. Manual action required'); }); it('labels idle + needsUpdate + autoUpdate as "Waiting for the next automatic check"', () => { diff --git a/frontend-modern/src/components/Settings/__tests__/infrastructureImportPlanModel.branchcov.test.ts b/frontend-modern/src/components/Settings/__tests__/infrastructureImportPlanModel.branchcov.test.ts index 1e9200aac..b30420f60 100644 --- a/frontend-modern/src/components/Settings/__tests__/infrastructureImportPlanModel.branchcov.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/infrastructureImportPlanModel.branchcov.test.ts @@ -353,7 +353,7 @@ describe('credentialLabel (via plan.credentialLabel and steps[1].detail)', () => ); expect(plan?.credentialLabel).toBe( - 'Run the Host Telemetry Agent handoff; Pulse will add the source after the agent reports.', + 'Run the Host Telemetry Agent handoff. Pulse will add the source after the agent reports.', ); }); @@ -365,7 +365,7 @@ describe('credentialLabel (via plan.credentialLabel and steps[1].detail)', () => ); expect(plan?.credentialLabel).toBe( - 'Run the API Inventory setup handoff; Pulse will add the source after setup completes.', + 'Run the API Inventory setup handoff. Pulse will add the source after setup completes.', ); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/patrolReadinessBanner.issue1640.test.ts b/frontend-modern/src/components/Settings/__tests__/patrolReadinessBanner.issue1640.test.ts index b2e0bedf4..7b8c51157 100644 --- a/frontend-modern/src/components/Settings/__tests__/patrolReadinessBanner.issue1640.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/patrolReadinessBanner.issue1640.test.ts @@ -106,7 +106,7 @@ describe('Patrol readiness banner presentation (#1640)', () => { }); expect(patrolReadinessBannerTone(transportOnly, false)).toBe('warning'); expect(patrolReadinessBannerHeadline(transportOnly, noStale)).toBe( - 'Provider connected; Patrol capability not verified', + 'Provider connected. Patrol capability not verified', ); }); diff --git a/frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts b/frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts index f2d794a23..35a8aa06a 100644 --- a/frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts +++ b/frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts @@ -461,7 +461,7 @@ const updaterPresentation = ( return { label: update.autoUpdate ? 'Update queued automatically' - : 'Update available; manual action required', + : 'Update available. Manual action required', waiting: needsUpdate && update.autoUpdate, }; case 'idle': @@ -552,7 +552,7 @@ const doctorTargetFromBinding = ( let commandBlockedReason: string | undefined; if ((needsUpdate || needsCredentialRepair) && hasDuplicateInstallation) { commandBlockedReason = - 'Multiple host-installed Pulse Agents are reporting from this machine. Pulse cannot safely choose a local service; inspect and remove or explicitly update the duplicate installation first.'; + 'Multiple host-installed Pulse Agents are reporting from this machine. Pulse cannot safely choose a local service. Inspect and remove or explicitly update the duplicate installation first.'; } else if (needsUpdate && !expectedVersion) { commandBlockedReason = 'No supported target version is available, so Pulse will not guess.'; } else if (needsUpdate && !parseAgentVersion(expectedVersion)) { @@ -832,7 +832,7 @@ export const formatInfrastructureAgentDoctorReport = ( .map((status) => `${counts[status]} ${DOCTOR_REPORT_STATUS_LABEL[status].toLowerCase()}`); const lines: string[] = [ `Pulse Agent Doctor report (${counts.total} agent${counts.total === 1 ? '' : 's'}${ - countParts.length > 0 ? `; ${countParts.join(', ')}` : '' + countParts.length > 0 ? ` · ${countParts.join(', ')}` : '' })`, ]; for (const target of targets) { diff --git a/frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts b/frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts index 6e9d1ae76..8ca519a15 100644 --- a/frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts +++ b/frontend-modern/src/components/Settings/infrastructureImportPlanModel.ts @@ -111,9 +111,9 @@ const credentialLabel = (formData: NodeModalFormData): string => { return 'Validate the supplied API token before saving.'; } if (formData.setupMode === 'agent') { - return 'Run the Host Telemetry Agent handoff; Pulse will add the source after the agent reports.'; + return 'Run the Host Telemetry Agent handoff. Pulse will add the source after the agent reports.'; } - return 'Run the API Inventory setup handoff; Pulse will add the source after setup completes.'; + return 'Run the API Inventory setup handoff. Pulse will add the source after setup completes.'; }; const coverageLabel = (nodeType: NodeModalNodeType, formData: NodeModalFormData): string => { diff --git a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx index 7c5d87ff2..65708c48d 100644 --- a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx +++ b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx @@ -317,7 +317,7 @@ export const INSTALL_PROFILE_OPTIONS: InstallProfileOption[] = [ value: 'proxmox-pve', label: 'Proxmox VE node', description: - 'Force Proxmox integration and register as a PVE node. Install on each cluster member for node-local telemetry such as temperatures and SMART data; use this profile plus command execution when Docker inside LXCs should be inventoried from the Proxmox host path.', + 'Force Proxmox integration and register as a PVE node. Install on each cluster member for node-local telemetry such as temperatures and SMART data. Use this profile plus command execution when Docker inside LXCs should be inventoried from the Proxmox host path.', flags: ['--enable-proxmox', '--proxmox-type pve'], }, { @@ -344,7 +344,7 @@ export const buildCommandsByPlatform = ( command: unixCommand, note: ( - Command auto-escalates with sudo when available; otherwise run from a root + Command auto-escalates with sudo when available. Otherwise run from a root shell (for example su -). Auto-detects your init system and works on Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more. diff --git a/frontend-modern/src/components/TokenRevealDialog.tsx b/frontend-modern/src/components/TokenRevealDialog.tsx index f0767b5a2..f9f2a77b6 100644 --- a/frontend-modern/src/components/TokenRevealDialog.tsx +++ b/frontend-modern/src/components/TokenRevealDialog.tsx @@ -60,7 +60,7 @@ export const TokenRevealDialog: Component = () => { const recordName = info.record?.name?.trim() || 'Untitled token'; const hint = info.note || - 'Copy this token now; Pulse will not display it again after you close this dialog.'; + 'Copy this token now. Pulse will not display it again after you close this dialog.'; const tokenHint = info.record?.prefix && info.record?.suffix ? `${info.record.prefix}…${info.record.suffix}` diff --git a/frontend-modern/src/components/UpdateBanner.test.tsx b/frontend-modern/src/components/UpdateBanner.test.tsx index 40afa7fd0..9261cc51e 100644 --- a/frontend-modern/src/components/UpdateBanner.test.tsx +++ b/frontend-modern/src/components/UpdateBanner.test.tsx @@ -192,7 +192,7 @@ describe('UpdateBanner Proxmox LXC update safety', () => { expect(await screen.findByRole('button', { name: 'Apply Update' })).toBeInTheDocument(); expect( - screen.getByText(/use Apply Update here; do not use a legacy community 'update' helper/), + screen.getByText(/use Apply Update here\. Do not use a legacy community 'update' helper/), ).toBeInTheDocument(); expect(screen.queryByText(/type 'update' in console/)).not.toBeInTheDocument(); diff --git a/frontend-modern/src/components/UpdateBanner.tsx b/frontend-modern/src/components/UpdateBanner.tsx index 43d5306a9..17a1257ff 100644 --- a/frontend-modern/src/components/UpdateBanner.tsx +++ b/frontend-modern/src/components/UpdateBanner.tsx @@ -117,7 +117,7 @@ export function UpdateBanner() { switch (deploymentType) { case 'proxmoxve': return updatePlan()?.canAutoUpdate - ? "ProxmoxVE LXC: use Apply Update here; do not use a legacy community 'update' helper" + ? "ProxmoxVE LXC: use Apply Update here. Do not use a legacy community 'update' helper" : 'ProxmoxVE LXC: use the signed Pulse installer, not a legacy community update helper'; case 'docker': return 'Docker: pull image and recreate container'; diff --git a/frontend-modern/src/components/patrol/ApprovalSection.tsx b/frontend-modern/src/components/patrol/ApprovalSection.tsx index 58f932e38..1b22b0f85 100644 --- a/frontend-modern/src/components/patrol/ApprovalSection.tsx +++ b/frontend-modern/src/components/patrol/ApprovalSection.tsx @@ -251,7 +251,7 @@ export const ApprovalSection: Component = (props) => { > {verificationStatus() === 'verified' ? 'Outcome verified' - : 'Execution finished; verification was not conclusive'} + : 'Execution finished. Verification was not conclusive'}
diff --git a/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts b/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts index a1bbd982b..be2b90874 100644 --- a/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts +++ b/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts @@ -170,7 +170,7 @@ export function useContainerUpdateButtonState(props: UpdateButtonProps) { markContainerQueued(props.agentId, props.containerId); props.onUpdateTriggered?.(); } else if (state === 'failed') { - const message = 'The update action failed; open Actions for the audit trail.'; + const message = 'The update action failed. Open Actions for the audit trail.'; setErrorMessage(message); markContainerUpdateError(props.agentId, props.containerId, message); } diff --git a/frontend-modern/src/content/help/ai.ts b/frontend-modern/src/content/help/ai.ts index ffaca79b0..04ce8d343 100644 --- a/frontend-modern/src/content/help/ai.ts +++ b/frontend-modern/src/content/help/ai.ts @@ -13,7 +13,7 @@ export const aiHelpContent: HelpContent[] = [ '- vLLM / llama.cpp: Self-hosted local inference servers\n' + '- Azure OpenAI: Enterprise Azure deployments\n' + '- Together, Anyscale, Fireworks: Alternative cloud providers\n\n' + - 'OpenRouter has a dedicated provider in Pulse; use this field for other OpenAI-compatible endpoints.', + 'OpenRouter has a dedicated provider in Pulse. Use this field for other OpenAI-compatible endpoints.', examples: [ 'http://localhost:8000/v1 (vLLM local)', 'https://your-resource.openai.azure.com (Azure)', diff --git a/frontend-modern/src/features/actions/ActionDecisionPacket.tsx b/frontend-modern/src/features/actions/ActionDecisionPacket.tsx index 35b522f62..c0c7c5d0f 100644 --- a/frontend-modern/src/features/actions/ActionDecisionPacket.tsx +++ b/frontend-modern/src/features/actions/ActionDecisionPacket.tsx @@ -351,7 +351,7 @@ export const ActionDecisionPacket: Component<{ : 'Pulse recorded the delivery attempt before sending it.'}

- Refreshing or reconnecting re-reads this action record; it does not create another + Refreshing or reconnecting re-reads this action record. It does not create another action.

diff --git a/frontend-modern/src/features/actions/ActionReviewDialog.tsx b/frontend-modern/src/features/actions/ActionReviewDialog.tsx index b6b9f277a..d438e909d 100644 --- a/frontend-modern/src/features/actions/ActionReviewDialog.tsx +++ b/frontend-modern/src/features/actions/ActionReviewDialog.tsx @@ -85,7 +85,7 @@ export const ActionReviewDialog: Component<{ if (!hasCurrentPolicyProvenance()) return 'This action has no current server policy provenance. Close it and create a new plan before approving or running anything.'; if (!aptParametersValid()) - return 'This host-maintenance action contains unexpected operator-selected parameters. Close it and create a new plan; do not approve or run this record.'; + return 'This host-maintenance action contains unexpected operator-selected parameters. Close it and create a new plan. Do not approve or run this record.'; if (readiness() && !readiness()!.ready) { return [readiness()!.message, readiness()!.remediation].filter(Boolean).join(' '); } diff --git a/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx b/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx index 344029000..8e5766389 100644 --- a/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx +++ b/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx @@ -304,7 +304,7 @@ describe('ActionDecisionPacket', () => { )); expect( within(screen.getByTestId('apt-action-facts')).getByText( - 'Yes — fact only; no reboot was authorized', + 'Yes — fact only. No reboot was authorized', ), ).toBeInTheDocument(); expect( diff --git a/frontend-modern/src/features/actions/__tests__/aptActionPresentation.test.ts b/frontend-modern/src/features/actions/__tests__/aptActionPresentation.test.ts index c49889a89..66dd34be7 100644 --- a/frontend-modern/src/features/actions/__tests__/aptActionPresentation.test.ts +++ b/frontend-modern/src/features/actions/__tests__/aptActionPresentation.test.ts @@ -119,7 +119,7 @@ describe('APT action presentation', () => { expect.arrayContaining([ { label: 'Updates remaining', value: '0' }, { label: 'Update system health', value: 'Known healthy' }, - { label: 'Reboot required', value: 'Yes — fact only; no reboot was authorized' }, + { label: 'Reboot required', value: 'Yes — fact only. No reboot was authorized' }, ]), ); expect(presentation?.nextStep).toContain('separate governed action'); diff --git a/frontend-modern/src/features/actions/aptActionPresentation.ts b/frontend-modern/src/features/actions/aptActionPresentation.ts index 03413ff58..ff7c181ad 100644 --- a/frontend-modern/src/features/actions/aptActionPresentation.ts +++ b/frontend-modern/src/features/actions/aptActionPresentation.ts @@ -70,7 +70,7 @@ const getUpdateFacts = (summary: string): APTActionPresentationFact[] => { { label: 'Recovery required', value: match[5] === 'true' ? 'Yes' : 'No' }, { label: 'Reboot required', - value: match[6] === 'true' ? 'Yes — fact only; no reboot was authorized' : 'No', + value: match[6] === 'true' ? 'Yes — fact only. No reboot was authorized' : 'No', }, ]; }; diff --git a/frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx b/frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx index 75f4bbd45..ef6e916db 100644 --- a/frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx +++ b/frontend-modern/src/features/alerts/AlertIntentPolicyPanel.tsx @@ -411,7 +411,7 @@ export function AlertIntentPolicyPanel(props: { placeholder="Use inherited behavior" /> - Blank inherits the existing policy; 0 alerts on the first stopped observation. + Blank inherits the existing policy. 0 alerts on the first stopped observation. { ); expect( screen.getByRole('menuitem', { - name: 'Restart unavailable: Docker inventory is stale; refresh inventory before running lifecycle actions.', + name: 'Restart unavailable: Docker inventory is stale. Refresh inventory before running lifecycle actions.', }), ).toBeDisabled(); }); diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.branchcov2.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.branchcov2.test.ts index 34d960845..b3922f41b 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.branchcov2.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.branchcov2.test.ts @@ -127,7 +127,7 @@ describe('dockerContainerLifecycleActions.branchcov2', () => { resource({ sourceStatus: { docker: { status: 'offline' } } }), 'restart', ), - ).toBe('Docker inventory is offline; refresh inventory before running lifecycle actions.'); + ).toBe('Docker inventory is offline. Refresh inventory before running lifecycle actions.'); }); it('blocks on a missing inventory status', () => { @@ -136,7 +136,7 @@ describe('dockerContainerLifecycleActions.branchcov2', () => { resource({ sourceStatus: { docker: { status: 'missing' } } }), 'restart', ), - ).toBe('Docker inventory is missing; refresh inventory before running lifecycle actions.'); + ).toBe('Docker inventory is missing. Refresh inventory before running lifecycle actions.'); }); it('blocks on an inventory error when the status itself is healthy', () => { diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.test.ts index 3572467b5..4715e61b0 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.test.ts @@ -126,7 +126,7 @@ describe('dockerContainerLifecycleActions', () => { resource({ sourceStatus: { docker: { status: 'stale' } } }), 'restart', ), - ).toBe('Docker inventory is stale; refresh inventory before running lifecycle actions.'); + ).toBe('Docker inventory is stale. Refresh inventory before running lifecycle actions.'); expect( getDockerContainerLifecycleDisabledReason( diff --git a/frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts b/frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts index 1f36662f5..48810366a 100644 --- a/frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts +++ b/frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts @@ -64,7 +64,7 @@ const sourceStatusDisabledReason = ( ): string | undefined => { const dockerStatus = normalizeToken(resource.sourceStatus?.docker?.status); if (STALE_SOURCE_STATUSES.has(dockerStatus)) { - return `${runtimeLabel} inventory is ${dockerStatus}; refresh inventory before running lifecycle actions.`; + return `${runtimeLabel} inventory is ${dockerStatus}. Refresh inventory before running lifecycle actions.`; } const dockerError = asTrimmedString(resource.sourceStatus?.docker?.error); if (dockerError) return `${runtimeLabel} inventory is not healthy: ${dockerError}`; diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index ce912b836..f59da7641 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -254,8 +254,8 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState role="status" class="mt-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-200" > - Requested {PATROL_AUTONOMY_POLICY_PRESENTATION[state.requestedAutonomyLevel()].label}; - effective mode is {selectedAutonomyPolicy().label}. Server status:{' '} + Requested {PATROL_AUTONOMY_POLICY_PRESENTATION[state.requestedAutonomyLevel()].label}. + Effective mode is {selectedAutonomyPolicy().label}. Server status:{' '} {state.autopilotStatus()?.code.replace(/_/g, ' ') || 'unavailable'}. diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts index d2404c7d5..690c13da3 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts @@ -115,7 +115,7 @@ describe('PatrolIntelligenceHeader', () => { expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({ monitor: { label: 'Watch only', - detail: 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + detail: 'Patrol checks infrastructure and reports issues only. It does not start fixes.', compactLabel: 'Watch only', }, approval: { @@ -126,7 +126,7 @@ describe('PatrolIntelligenceHeader', () => { assisted: { label: 'Safe auto-fix', detail: - 'Patrol can run low- or medium-risk fixes allowed by policy; higher-risk work still asks first.', + 'Patrol can run low- or medium-risk fixes allowed by policy. Higher-risk work still asks first.', compactLabel: 'Safe auto-fix', }, full: { diff --git a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.branchcov2.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.branchcov2.test.ts index 60a80c6e5..04238b941 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.branchcov2.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.branchcov2.test.ts @@ -65,7 +65,7 @@ describe('patrolControlPresentation branch coverage (set 2)', () => { tone: 'warning', }, { - detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + detail: 'Current Patrol findings and approvals stay in Patrol. None are waiting now.', id: 'open-work', label: 'No Patrol work waiting', tone: 'warning', @@ -120,7 +120,7 @@ describe('patrolControlPresentation branch coverage (set 2)', () => { tone: 'success', }, { - detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + detail: 'Current Patrol findings and approvals stay in Patrol. None are waiting now.', id: 'open-work', label: 'No Patrol work waiting', tone: 'success', @@ -170,7 +170,7 @@ describe('patrolControlPresentation branch coverage (set 2)', () => { tone: 'warning', }, { - detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + detail: 'Current Patrol findings and approvals stay in Patrol. None are waiting now.', id: 'open-work', label: 'No Patrol work waiting', tone: 'success', diff --git a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts index e7490b192..ab2a4d4fc 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts @@ -99,7 +99,7 @@ describe('patrolControlPresentation', () => { expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({ monitor: { label: 'Watch only', - detail: 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + detail: 'Patrol checks infrastructure and reports issues only. It does not start fixes.', compactLabel: 'Watch only', }, approval: { @@ -110,7 +110,7 @@ describe('patrolControlPresentation', () => { assisted: { label: 'Safe auto-fix', detail: - 'Patrol can run low- or medium-risk fixes allowed by policy; higher-risk work still asks first.', + 'Patrol can run low- or medium-risk fixes allowed by policy. Higher-risk work still asks first.', compactLabel: 'Safe auto-fix', }, full: { @@ -309,7 +309,7 @@ describe('patrolControlPresentation', () => { }, { detail: - 'Patrol has not completed a fresh full check; run Patrol to refresh current coverage.', + 'Patrol has not completed a fresh full check. Run Patrol to refresh current coverage.', id: 'stale-protection', label: 'Coverage stale', tone: 'warning', @@ -404,7 +404,7 @@ describe('patrolControlPresentation', () => { tone: 'success', }, { - detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + detail: 'Current Patrol findings and approvals stay in Patrol. None are waiting now.', id: 'open-work', label: 'No Patrol work waiting', tone: 'success', diff --git a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov.test.ts index 3b26c3b5a..f5e815a6d 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov.test.ts @@ -612,7 +612,7 @@ describe('buildPatrolAssistantFindingBriefing (branch coverage)', () => { }); expect(briefing?.safetyNote).toBe( - 'Command details stay in approval context; execution requires the governed approval flow.', + 'Command details stay in approval context. Execution requires the governed approval flow.', ); }); diff --git a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov0723pm.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov0723pm.test.ts index b58bad4b8..c48041e97 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov0723pm.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.branchcov0723pm.test.ts @@ -226,7 +226,7 @@ describe('buildPatrolRunAssistantHandoff (residual branches)', () => { } as unknown as PatrolRunRecord); expect(handoff.context.briefing?.evidence?.[0]).toBe( - '1 new finding; 1 existing finding; 1 resolved finding; 1 rejected finding; 1 auto-remediation; 1 error', + '1 new finding · 1 existing finding · 1 resolved finding · 1 rejected finding · 1 auto-remediation · 1 error', ); }); @@ -244,7 +244,7 @@ describe('buildPatrolRunAssistantHandoff (residual branches)', () => { } as unknown as PatrolRunRecord); expect(handoff.context.briefing?.evidence?.[0]).toBe( - '2 new findings; 2 existing findings; 2 resolved findings; 2 rejected findings; 2 auto-remediations; 2 errors', + '2 new findings · 2 existing findings · 2 resolved findings · 2 rejected findings · 2 auto-remediations · 2 errors', ); }); diff --git a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts index cfc2bf2f5..5d4b4ebd8 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts @@ -391,7 +391,7 @@ describe('patrolInvestigationContextModel', () => { subject: 'Issues detected', actionLabel: '1 governed action reference attached', safetyNote: - 'Review action posture in the governed flow; raw command payloads stay out of Assistant.', + 'Review action posture in the governed flow · raw command payloads stay out of Assistant.', }); expect(JSON.stringify(handoff)).not.toContain('systemctl restart workload.service'); }); @@ -465,7 +465,7 @@ describe('patrolInvestigationContextModel', () => { expect(handoff.context.briefing).toMatchObject({ actionLabel: 'Discuss Patrol coverage', safetyNote: - 'Assistant can review the coverage evidence; Patrol runs, diagnostics, and remediation remain governed controls.', + 'Assistant can review the coverage evidence. Patrol runs, diagnostics, and remediation remain governed controls.', }); expect(handoff.context.briefing?.actionHref).toBeUndefined(); expect(handoff.context.handoffContext).toContain('Assessment: Coverage incomplete'); @@ -621,7 +621,7 @@ describe('patrolInvestigationContextModel', () => { expect(handoff.context.briefing).toMatchObject({ actionLabel: '1 pending governed approval attached', safetyNote: - 'Review approvals in the governed flow; approval policy is attached; dry-run posture is attached; destructive actions remain approval-bound; raw command payloads stay out of Assistant.', + 'Review approvals in the governed flow · approval policy is attached · dry-run posture is attached · destructive actions remain approval-bound · raw command payloads stay out of Assistant.', }); expect(JSON.stringify(handoff)).not.toContain('systemctl restart workload.service'); }); @@ -1190,13 +1190,13 @@ describe('patrolInvestigationContextModel', () => { statusLabel: 'Completed · Fix Queued · High confidence', detailLines: [ 'Backup job saturated CPU.', - 'Existing action artifact: Restart the workload service; medium risk; 1 command recorded for approval context; destructive action artifact', + 'Existing action artifact: Restart the workload service · medium risk · 1 command recorded for approval context · destructive action artifact', ], evidence: ['CPU stayed above 95% for 10 minutes', 'Verified: CPU returned below 50%'], actionLabel: undefined, commandSummary: '1 command recorded for approval context', safetyNote: - 'Command details stay in approval context; destructive actions require governed approval.', + 'Command details stay in approval context. Destructive actions require governed approval.', }); expect(JSON.stringify(briefing)).not.toContain('systemctl restart workload.service'); vi.useRealTimers(); @@ -1249,7 +1249,7 @@ describe('patrolInvestigationContextModel', () => { ); expect(modelContext).toContain('Existing Action Artifact: Restore web service'); expect(modelContext).toContain( - '2 commands recorded for governed plan review; 1 rollback command recorded', + '2 commands recorded for governed plan review · 1 rollback command recorded', ); expect(modelContext).toContain('Treat this as approval state, not remediation guidance.'); expect(modelContext).toContain('Do not assume any Patrol-authored action is correct.'); @@ -1267,10 +1267,10 @@ describe('patrolInvestigationContextModel', () => { expect(briefing.evidence).toBeUndefined(); expect(briefing.actionLabel).toBeUndefined(); expect(briefing.commandSummary).toBe( - '2 commands recorded for governed plan review; 1 rollback command recorded', + '2 commands recorded for governed plan review · 1 rollback command recorded', ); expect(briefing.safetyNote).toBe( - 'Assistant should decide remediation from evidence; command execution requires governed approval.', + 'Assistant should decide remediation from evidence. Command execution requires governed approval.', ); expect(JSON.stringify(briefing)).not.toContain('systemctl'); }); @@ -1467,13 +1467,13 @@ describe('patrolInvestigationContextModel', () => { subject: 'CPU saturation on node-1', statusLabel: 'Fix Queued', detailLines: [ - 'Existing action artifact: Restart workload service; target node-1; high risk; 1 command recorded for approval context; destructive action artifact; rationale service is wedged', + 'Existing action artifact: Restart workload service · target node-1 · high risk · 1 command recorded for approval context · destructive action artifact · rationale service is wedged', ], evidence: [], actionLabel: undefined, commandSummary: '1 command recorded for approval context', safetyNote: - 'Command details stay in approval context; destructive actions require governed approval.', + 'Command details stay in approval context. Destructive actions require governed approval.', }); }); }); diff --git a/frontend-modern/src/features/patrol/patrolControlPresentation.ts b/frontend-modern/src/features/patrol/patrolControlPresentation.ts index d482d86dd..a3fbe70bd 100644 --- a/frontend-modern/src/features/patrol/patrolControlPresentation.ts +++ b/frontend-modern/src/features/patrol/patrolControlPresentation.ts @@ -20,7 +20,7 @@ export const PATROL_AUTONOMY_POLICY_PRESENTATION: Record< > = { monitor: { label: 'Watch only', - detail: 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + detail: 'Patrol checks infrastructure and reports issues only. It does not start fixes.', compactLabel: 'Watch only', }, approval: { @@ -31,7 +31,7 @@ export const PATROL_AUTONOMY_POLICY_PRESENTATION: Record< assisted: { label: 'Safe auto-fix', detail: - 'Patrol can run low- or medium-risk fixes allowed by policy; higher-risk work still asks first.', + 'Patrol can run low- or medium-risk fixes allowed by policy. Higher-risk work still asks first.', compactLabel: 'Safe auto-fix', }, full: { @@ -464,7 +464,7 @@ export function getPatrolWorkspaceWorkGroups( id: 'stale-protection', label: 'Coverage stale', detail: - 'Patrol has not completed a fresh full check; run Patrol to refresh current coverage.', + 'Patrol has not completed a fresh full check. Run Patrol to refresh current coverage.', tone: 'warning', }); } @@ -529,7 +529,7 @@ export function getMonitorContextPatrolProtectionPosture( { id: 'open-work', label: 'No Patrol work waiting', - detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + detail: 'Current Patrol findings and approvals stay in Patrol. None are waiting now.', tone: healthyTone, }, ]; diff --git a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts index c5d212b68..42fc63def 100644 --- a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts +++ b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts @@ -672,7 +672,7 @@ export function buildPatrolConfigurationFailureHandoff( evidence: formatConfigurationFailureDetails(input.details).slice(0, 4), actionLabel: `Review ${issueLabel}`, safetyNote: - 'Assistant can explain the Patrol mode state; provider changes, retries, and remediation remain operator-controlled.', + 'Assistant can explain the Patrol mode state. Provider changes, retries, and remediation remain operator-controlled.', }, context: { source: 'pulse-patrol-configuration-failure', @@ -784,7 +784,7 @@ function buildPatrolAssessmentActionPosture( return { actionLabel: 'Discuss Patrol coverage', safetyNote: - 'Assistant can review the coverage evidence; Patrol runs, diagnostics, and remediation remain governed controls.', + 'Assistant can review the coverage evidence. Patrol runs, diagnostics, and remediation remain governed controls.', }; } @@ -811,7 +811,7 @@ function formatAssessmentActionSafetyNote(input: { parts.push('destructive actions remain approval-bound'); } parts.push('raw command payloads stay out of Assistant'); - return `${parts.join('; ')}.`; + return `${parts.join(' · ')}.`; } function assessmentHasCoverageGap(input: PatrolAssessmentAssistantHandoffInput): boolean { @@ -924,7 +924,7 @@ function buildPatrolRunAssistantBriefing( evidence: [outcomes, run.findings_summary, analysis].filter(isNonEmptyString).slice(0, 4), actionLabel: runtimeFailure ? 'Review Patrol runtime failure' : 'Discuss Patrol run outcome', safetyNote: - 'Assistant can explain the Patrol run context; retries, configuration changes, and remediation remain operator-controlled.', + 'Assistant can explain the Patrol run context. Retries, configuration changes, and remediation remain operator-controlled.', }; } @@ -1492,7 +1492,7 @@ function formatAssessmentVerification( : undefined, ] .filter(isNonEmptyString) - .join('; '); + .join(' · '); } function formatAssessmentRecency(input: PatrolAssessmentAssistantHandoffInput): string | undefined { @@ -1516,7 +1516,7 @@ function formatAssessmentLatestRun( latestRun.findingsSnapshotAvailable === false ? 'finding record unavailable' : undefined, ] .filter(isNonEmptyString) - .join('; '); + .join(' · '); } function formatAssessmentFindingEvidence( @@ -1594,7 +1594,7 @@ function formatAssessmentRecentChangeContextLine(change: ResourceChange, index: formatAssessmentRecentChangeRelatedResources(change), ].filter(isNonEmptyString); - return `Recent Change ${index}: ${parts.join('; ')}`; + return `Recent Change ${index}: ${parts.join(' · ')}`; } function formatAssessmentRecentChangeRelatedResources(change: ResourceChange): string | undefined { @@ -1646,7 +1646,7 @@ function formatAssessmentCorrelationContextLine( : undefined, ].filter(isNonEmptyString); - return `Correlation ${index}: ${parts.join('; ')}`; + return `Correlation ${index}: ${parts.join(' · ')}`; } function formatAssessmentRecentChangeSummary(change: ResourceChange): string { @@ -1765,7 +1765,7 @@ function formatAssessmentFindingContextLine( ...recordParts, ].filter(isNonEmptyString); - return `Finding ${index}: ${parts.join('; ')}`; + return `Finding ${index}: ${parts.join(' · ')}`; } function buildPatrolAssistantFindingModelContext( @@ -1823,7 +1823,7 @@ function buildPatrolAssistantFindingModelContext( formatContextLine('Status', statusParts.join(' · ')), formatContextLine('Detected At', input.detectedAt), formatContextLine('Last Seen At', input.lastSeenAt), - formatContextLine('Recurrence', raisedParts.join('; ')), + formatContextLine('Recurrence', raisedParts.join(' · ')), formatContextLine('Description', input.description), formatContextLine('Investigation Record', input.investigationRecord?.id), formatContextLine('Investigation Status', record.statusLabel), @@ -1862,7 +1862,7 @@ function buildPatrolAssistantFindingModelContext( formatContextLine('Action Preflight', pendingApproval.actionPreflight), formatContextLine('Dry-Run Posture', pendingApproval.actionDryRunSummary), formatContextLine('Existing Action Artifact', actionArtifactFacts), - 'Command Boundary: Command details stay in governed approval or remediation context; this model-only handoff may include command counts but not raw command text.', + 'Command Boundary: Command details stay in governed approval or remediation context. This model-only handoff may include command counts but not raw command text.', 'Model Boundary: This Patrol finding handoff is model-only context for explanation and review. Diagnostics, remediation, and command execution require explicit governed approval.', ] .filter(isNonEmptyString) @@ -1972,7 +1972,7 @@ export function buildPatrolRemediationPlanAssistantModelContext( commandSummary ? `Governed Action Context: ${commandSummary}. Treat this as approval state, not remediation guidance.` : undefined, - 'Model Boundary: Do not assume any Patrol-authored action is correct. Use the finding evidence, available tools, and current operational state to decide the remediation. Any state-changing command must go through governed approval; do not infer, repeat, or execute raw command text from this chat handoff.', + 'Model Boundary: Do not assume any Patrol-authored action is correct. Use the finding evidence, available tools, and current operational state to decide the remediation. Any state-changing command must go through governed approval. Do not infer, repeat, or execute raw command text from this chat handoff.', ] .filter(isNonEmptyString) .join('\n'); @@ -2005,7 +2005,7 @@ export function buildPatrolRemediationPlanAssistantBriefing( ].filter(isNonEmptyString), commandSummary, safetyNote: commandSummary - ? 'Assistant should decide remediation from evidence; command execution requires governed approval.' + ? 'Assistant should decide remediation from evidence. Command execution requires governed approval.' : 'Assistant should decide remediation from evidence before any governed action.', }; } @@ -2074,10 +2074,10 @@ function buildPatrolAssistantSafetyNote( const hasCommands = Boolean(proposedFix?.commandSummary); const isDestructive = Boolean(proposedFix?.destructive); if (hasCommands && isDestructive) { - return 'Command details stay in approval context; destructive actions require governed approval.'; + return 'Command details stay in approval context. Destructive actions require governed approval.'; } if (hasCommands && pendingApproval?.id) { - return 'Command details stay in approval context; execution requires the governed approval flow.'; + return 'Command details stay in approval context. Execution requires the governed approval flow.'; } if (hasCommands) { return 'Command details stay in approval context.'; @@ -2202,7 +2202,7 @@ function formatPlanCommandSummary(plan: RemediationPlan): string | undefined { : `${rollbackCount} rollback commands recorded`, ); } - return parts.join('; '); + return parts.join(' · '); } function formatBriefingStringList( @@ -2226,7 +2226,7 @@ function formatBriefingStringList( if (remaining > 0) { parts.push(`${remaining} more ${itemName || 'items'}`); } - return parts.join('; '); + return parts.join(' · '); } function formatIdentifierLabel(value?: string | null): string | undefined { diff --git a/frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx b/frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx index 20da40c3a..0f1863e5c 100644 --- a/frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx +++ b/frontend-modern/src/features/standalone/HostIdentityConflictNotice.tsx @@ -40,7 +40,7 @@ export function HostIdentityConflictNotice(props: HostIdentityConflictNoticeProp `Give one of them a fresh machine-id and restart its agent to monitor them separately.` ); } - const labels = props.hosts.map(hostLabel).join('; '); + const labels = props.hosts.map(hostLabel).join(' · '); return ( `${count()} hosts are each receiving reports from more than one machine. ` + `They are likely cloned from the same template with the same /etc/machine-id, so their reports overwrite each other. ` + diff --git a/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.coverage2.test.ts b/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.coverage2.test.ts index 3cf0943bc..f7f382694 100644 --- a/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.coverage2.test.ts +++ b/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.coverage2.test.ts @@ -466,7 +466,7 @@ describe('agentMachineTableModel coverage2', () => { }), ); expect(result?.title).toBe( - 'Thermal pressure nominal via throttling; limits: cpu package 80%, disk array 90%, gpu hotspot 95%', + 'Thermal pressure nominal via throttling · limits: cpu package 80%, disk array 90%, gpu hotspot 95%', ); }); }); diff --git a/frontend-modern/src/features/standalone/agentMachineTableModel.ts b/frontend-modern/src/features/standalone/agentMachineTableModel.ts index 905ff3560..f8307964e 100644 --- a/frontend-modern/src/features/standalone/agentMachineTableModel.ts +++ b/frontend-modern/src/features/standalone/agentMachineTableModel.ts @@ -639,7 +639,7 @@ export const getAgentMachineThermalPressurePresentation = ( .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => `${key.replace(/_/g, ' ')} ${Math.round(value)}%`); const sourceSummary = source ? ` via ${source}` : ''; - const limitSummary = limits.length > 0 ? `; limits: ${limits.join(', ')}` : ''; + const limitSummary = limits.length > 0 ? ` · limits: ${limits.join(', ')}` : ''; return { label, diff --git a/frontend-modern/src/i18n/index.ts b/frontend-modern/src/i18n/index.ts index 17fca2d69..3b6f29d14 100644 --- a/frontend-modern/src/i18n/index.ts +++ b/frontend-modern/src/i18n/index.ts @@ -93,7 +93,7 @@ function getDeferredLocale(locale: SupportedLocale): DeferredLocale | null { function handleLocaleCatalogLoadFailure(locale: SupportedLocale, error: unknown): void { if (typeof console === 'undefined') return; - console.warn(`Unable to load ${locale} locale catalog; falling back to English.`, error); + console.warn(`Unable to load ${locale} locale catalog. Falling back to English.`, error); } function scheduleLocaleCatalogLoad(locale: SupportedLocale): void { diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index 276de6bca..d9928ae6a 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -21,7 +21,7 @@ export const DE_MESSAGE_OVERRIDES = { 'alerts.assistant.button.full': 'Pulse Assistant zu dieser Warnmeldung fragen', 'alerts.assistant.button.text': 'Pulse Assistant fragen', 'alerts.assistant.detail.currentMetric': - 'Aktueller Wert {currentValue}; Schwellwert {thresholdValue}', + 'Aktueller Wert {currentValue} · Schwellwert {thresholdValue}', 'alerts.assistant.detail.message': 'Meldung: {message}', 'alerts.assistant.detail.node': 'Knoten: {node}', 'alerts.assistant.duration.hoursMinutes': '{hours}h {minutes}m', @@ -235,7 +235,7 @@ export const DE_MESSAGE_OVERRIDES = { 'setup.security.nextScreen.itemApiToken': 'Ein Admin-API-Token fuer Automatisierung', 'setup.security.nextScreen.itemCredentials': 'Ihr Benutzername und Passwort', 'setup.security.nextScreen.saveOnce': - 'Speichern Sie sie vor dem Fortfahren; sie werden nur einmal angezeigt.', + 'Speichern Sie sie vor dem Fortfahren. Sie werden nur einmal angezeigt.', 'setup.security.nextScreen.title': 'Auf dem naechsten Bildschirm', 'setup.security.passwordMode.autoGenerate': 'Automatisch erzeugen', 'setup.security.placeholder.password': 'Passwort (mind. 12 Zeichen)', @@ -285,7 +285,7 @@ export const DE_MESSAGE_OVERRIDES = { 'setup.welcome.placeholder.bootstrapToken': 'Bootstrap-Token einfuegen', 'setup.welcome.success.commandCopied': 'Befehl in Zwischenablage kopiert', 'setup.welcome.telemetryNotice.description': - 'Ausgehende Nutzungstelemetrie ist standardmaessig aktiviert. Pulse sendet einen verzoegerten Start-Ping und einen taeglichen Heartbeat mit einer rotierenden pseudonymen Installations-ID, Release-/Runtime-Details, aggregierten Zaehlern und Funktionsflags. Um sie vor jedem Ping zu deaktivieren, setzen Sie PULSE_TELEMETRY=false, bevor Sie Pulse starten; spaeter koennen Sie sie auch in den Einstellungen ausschalten.', + 'Ausgehende Nutzungstelemetrie ist standardmaessig aktiviert. Pulse sendet einen verzoegerten Start-Ping und einen taeglichen Heartbeat mit einer rotierenden pseudonymen Installations-ID, Release-/Runtime-Details, aggregierten Zaehlern und Funktionsflags. Um sie vor jedem Ping zu deaktivieren, setzen Sie PULSE_TELEMETRY=false, bevor Sie Pulse starten. Spaeter koennen Sie sie auch in den Einstellungen ausschalten.', 'setup.welcome.telemetryNotice.detailsLink': 'Details', 'setup.welcome.telemetryNotice.title': 'Nutzungstelemetrie ist standardmaessig aktiviert', 'setup.welcome.tokenHelp.afterVerify': diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index aa63bf0f0..4909d0c3d 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -20,7 +20,7 @@ export const ES_MESSAGE_OVERRIDES = { 'alerts.assistant.action.investigate': 'Investigar alerta {alertIdentifier}', 'alerts.assistant.button.full': 'Preguntar a Pulse Assistant sobre esta alerta', 'alerts.assistant.button.text': 'Preguntar a Pulse Assistant', - 'alerts.assistant.detail.currentMetric': 'Valor actual {currentValue}; umbral {thresholdValue}', + 'alerts.assistant.detail.currentMetric': 'Valor actual {currentValue} · umbral {thresholdValue}', 'alerts.assistant.detail.message': 'Mensaje: {message}', 'alerts.assistant.detail.node': 'Nodo: {node}', 'alerts.assistant.duration.hoursMinutes': '{hours}h {minutes}m', @@ -156,18 +156,18 @@ export const ES_MESSAGE_OVERRIDES = { 'setup.completion.credentials.continuation.connected': 'Infraestructura o Agregar infraestructura.', 'setup.completion.credentials.description': - 'Guarda el inicio de sesión de administrador y el token de API antes de salir de esta pantalla; luego continúa en {destination}', + 'Guarda el inicio de sesión de administrador y el token de API antes de salir de esta pantalla. Luego continúa en {destination}', 'setup.completion.credentials.passwordLabel': 'Contraseña', 'setup.completion.credentials.title': 'Credenciales que debes guardar ahora', 'setup.completion.credentials.usernameLabel': 'Usuario', 'setup.completion.download.content': 'Credenciales de Pulse\n==================\nGenerado: {generatedAt}\n\nInicio de sesión web:\n----------\nURL: {baseUrl}\nUsuario: {username}\nContraseña: {password}\n\nToken de API de administrador:\n----------------\n{apiToken}\n\nInfraestructura:\n---------------\n{infrastructureUrl}\n\nUsa Agregar infraestructura para elegir una API de plataforma, Pulse Agent o ambos\npara el primer sistema que Pulse debe supervisar.\n\nGuarda estas credenciales en un lugar seguro.\n', 'setup.completion.hero.connected.description.agent': - 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra fuente Pulse Agent o API de plataforma.', + 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema. Luego vuelve a Agregar infraestructura cuando quieras otra fuente Pulse Agent o API de plataforma.', 'setup.completion.hero.connected.description.api': - 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma o fuente Pulse Agent.', + 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema. Luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma o fuente Pulse Agent.', 'setup.completion.hero.connected.description.both': - 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma, fuente Agent o ambas.', + 'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema. Luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma, fuente Agent o ambas.', 'setup.completion.hero.connected.title': 'Primer sistema supervisado conectado', 'setup.completion.hero.empty.description': 'Tu cuenta de administrador está lista. Ahora elige cómo entrará el primer sistema en el modelo unificado de infraestructura: inventario por API de plataforma, telemetría de Pulse Agent o ambos.', @@ -227,7 +227,7 @@ export const ES_MESSAGE_OVERRIDES = { 'setup.security.minimumPasswordHelp': 'Mínimo 12 caracteres.', 'setup.security.nextScreen.itemApiToken': 'Un token de API de administrador para automatización', 'setup.security.nextScreen.itemCredentials': 'Tu usuario y contraseña', - 'setup.security.nextScreen.saveOnce': 'Guárdalos antes de continuar; se muestran una sola vez.', + 'setup.security.nextScreen.saveOnce': 'Guárdalos antes de continuar. Se muestran una sola vez.', 'setup.security.nextScreen.title': 'En la siguiente pantalla', 'setup.security.passwordMode.autoGenerate': 'Generar automáticamente', 'setup.security.placeholder.password': 'Contraseña (mínimo 12 caracteres)', @@ -277,7 +277,7 @@ export const ES_MESSAGE_OVERRIDES = { 'setup.welcome.placeholder.bootstrapToken': 'Pega tu token de bootstrap', 'setup.welcome.success.commandCopied': 'Comando copiado al portapapeles', 'setup.welcome.telemetryNotice.description': - 'La telemetría de uso saliente está activada por defecto. Pulse envía un ping de inicio retrasado y un heartbeat diario con un ID de instalación seudónimo rotativo, detalles de versión/runtime, conteos agregados y flags de funciones. Para desactivarla antes de cualquier ping, define PULSE_TELEMETRY=false antes de iniciar Pulse; también puedes desactivarla luego en Ajustes.', + 'La telemetría de uso saliente está activada por defecto. Pulse envía un ping de inicio retrasado y un heartbeat diario con un ID de instalación seudónimo rotativo, detalles de versión/runtime, conteos agregados y flags de funciones. Para desactivarla antes de cualquier ping, define PULSE_TELEMETRY=false antes de iniciar Pulse. También puedes desactivarla luego en Ajustes.', 'setup.welcome.telemetryNotice.detailsLink': 'Detalles completos', 'setup.welcome.telemetryNotice.title': 'La telemetría de uso está activada por defecto', 'setup.welcome.tokenHelp.afterVerify': diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index 1d828c6cc..f5ece73d6 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -116,7 +116,7 @@ export const EN_MESSAGES = { 'alerts.assistant.button.full': 'Ask Pulse Assistant about this alert', 'alerts.assistant.button.text': 'Ask Pulse Assistant', 'alerts.assistant.detail.currentMetric': - 'Current value {currentValue}; threshold {thresholdValue}', + 'Current value {currentValue} · threshold {thresholdValue}', 'alerts.assistant.detail.message': 'Message: {message}', 'alerts.assistant.detail.node': 'Node: {node}', 'alerts.assistant.duration.hoursMinutes': '{hours}h {minutes}m', @@ -271,7 +271,7 @@ export const EN_MESSAGES = { 'setup.welcome.placeholder.bootstrapToken': 'Paste your bootstrap token', 'setup.welcome.success.commandCopied': 'Command copied to clipboard', 'setup.welcome.telemetryNotice.description': - 'Outbound usage telemetry is on by default. Pulse sends a delayed startup ping and daily heartbeat with a rotating pseudonymous install ID, release/runtime details, aggregate counts, and feature flags. To disable it before any ping, set PULSE_TELEMETRY=false before starting Pulse; you can also turn it off later in Settings.', + 'Outbound usage telemetry is on by default. Pulse sends a delayed startup ping and daily heartbeat with a rotating pseudonymous install ID, release/runtime details, aggregate counts, and feature flags. To disable it before any ping, set PULSE_TELEMETRY=false before starting Pulse. You can also turn it off later in Settings.', 'setup.welcome.telemetryNotice.detailsLink': 'Full details', 'setup.welcome.telemetryNotice.title': 'Usage telemetry is enabled by default', 'setup.welcome.tokenHelp.afterVerify': diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index d93d90c59..d600af789 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -717,7 +717,7 @@ describe('AIIntelligence entitlement gating', () => { expect(patrolControl.getAllByText('Watch only').length).toBeGreaterThan(0); expect( patrolControl.getByText( - 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + 'Patrol checks infrastructure and reports issues only. It does not start fixes.', ), ).toBeInTheDocument(); expect(patrolControl.queryByRole('button', { name: 'Limits' })).not.toBeInTheDocument(); @@ -1262,7 +1262,7 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.getByRole('button', { name: 'Autopilot' })).not.toBeDisabled(); expect( screen.getByText( - 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + 'Patrol checks infrastructure and reports issues only. It does not start fixes.', ), ).toBeInTheDocument(); expect(screen.queryByText('Patrol handles')).not.toBeInTheDocument(); @@ -2006,7 +2006,7 @@ describe('AIIntelligence entitlement gating', () => { expect(patrolControl.getAllByText('Watch only').length).toBeGreaterThan(0); expect( patrolControl.getByText( - 'Patrol checks infrastructure and reports issues only; it does not start fixes.', + 'Patrol checks infrastructure and reports issues only. It does not start fixes.', ), ).toBeInTheDocument(); expect(patrolControl.getByRole('button', { name: 'Ask first' })).not.toBeDisabled(); diff --git a/frontend-modern/src/stores/websocket.ts b/frontend-modern/src/stores/websocket.ts index 664c738e7..166adf81e 100644 --- a/frontend-modern/src/stores/websocket.ts +++ b/frontend-modern/src/stores/websocket.ts @@ -289,7 +289,7 @@ export function createWebSocketStore(url: string) { const previousAckTime = pending.previousAckTime || ''; if (serverAckTime === previousAckTime) { logger.debug( - `Server ack time for alert ${id} unchanged (${serverAckTime}); treating as confirmed`, + `Server ack time for alert ${id} unchanged (${serverAckTime}). Treating as confirmed`, ); } } else if (alert.acknowledged) { @@ -695,7 +695,7 @@ export function createWebSocketStore(url: string) { // hydration and remain delta-free until a full socket snapshot lands. rawServerResources = null; oversizedSnapshotObserved = true; - logger.warn('Server withheld an oversized state payload; recovering over REST', { + logger.warn('Server withheld an oversized state payload. Recovering over REST', { supersedes: message.data.supersedes, bytes: message.data.bytes, maxBytes: message.data.maxBytes, @@ -873,7 +873,7 @@ export function createWebSocketStore(url: string) { // The socket has already proven it cannot deliver this estate's snapshot. // `requestData` would just queue another frame the guard drops, which is // what left large estates stuck on an empty UI, retrying every 30s. - logger.warn('Recovering full state over REST; socket snapshot exceeds the inbound limit'); + logger.warn('Recovering full state over REST. Socket snapshot exceeds the inbound limit'); void hydrateFullStateFromREST(connectionId); return; } @@ -881,7 +881,7 @@ export function createWebSocketStore(url: string) { if (now - lastFullStateRecoveryAt < 30000) return; lastFullStateRecoveryAt = now; - logger.warn('Received resource delta without a full snapshot baseline; requesting full state'); + logger.warn('Received resource delta without a full snapshot baseline. Requesting full state'); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'requestData' })); } diff --git a/frontend-modern/src/utils/__tests__/actionAuditPresentation.branchcov0712c.test.ts b/frontend-modern/src/utils/__tests__/actionAuditPresentation.branchcov0712c.test.ts index 5818f7c63..b5e006372 100644 --- a/frontend-modern/src/utils/__tests__/actionAuditPresentation.branchcov0712c.test.ts +++ b/frontend-modern/src/utils/__tests__/actionAuditPresentation.branchcov0712c.test.ts @@ -185,7 +185,7 @@ describe('getActionAuditVerificationOutcomePresentation — actionResultV2 verif expect(presentation).toStrictEqual({ label: 'Confirmation lacks an evidence source', detail: - 'The record says confirmed but provides no evidence source; do not treat it as independently verified.', + 'The record says confirmed but provides no evidence source. Do not treat it as independently verified.', evidenceSummary: 'No additional verification summary. Source: No evidence source.', className: 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300', diff --git a/frontend-modern/src/utils/__tests__/aiFindingPresentation.branchcov0717.test.ts b/frontend-modern/src/utils/__tests__/aiFindingPresentation.branchcov0717.test.ts index 816c2fb54..8c74938dc 100644 --- a/frontend-modern/src/utils/__tests__/aiFindingPresentation.branchcov0717.test.ts +++ b/frontend-modern/src/utils/__tests__/aiFindingPresentation.branchcov0717.test.ts @@ -752,7 +752,7 @@ describe('aiFindingPresentation branch coverage (supplemental)', () => { ).toStrictEqual({ stage: 'approval', label: 'Run approved action', - detail: 'The typed action is approved; execution remains a separate operator step.', + detail: 'The typed action is approved. Execution remains a separate operator step.', tone: 'success', }); }); diff --git a/frontend-modern/src/utils/__tests__/alertThresholdsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertThresholdsPresentation.test.ts index fe8ce6330..9d18a65d2 100644 --- a/frontend-modern/src/utils/__tests__/alertThresholdsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertThresholdsPresentation.test.ts @@ -101,7 +101,7 @@ describe('alertThresholdsPresentation', () => { inheritanceGuidance: 'Resources inherit their group defaults until you save a custom override.', bulkGuidance: 'Select multiple resources to apply the same threshold changes in bulk.', - collapseHint: 'Resource groups start collapsed; open only the group you need.', + collapseHint: 'Resource groups start collapsed. Open only the group you need.', }); expect(getAlertThresholdsOverridesPresentation()).toEqual({ title: 'Custom overrides', @@ -147,7 +147,7 @@ describe('alertThresholdsPresentation', () => { expect(getAlertThresholdsDockerIgnoredPrefixesPresentation()).toEqual({ title: 'Ignored container patterns', description: - 'Containers whose name or ID matches any pattern below are skipped for container alerts. A bare entry matches as a prefix; use * as a wildcard (runner-*, *-dev, *staging*). Enter one pattern per line; matching is case-insensitive.', + 'Containers whose name or ID matches any pattern below are skipped for container alerts. A bare entry matches as a prefix. Use * as a wildcard (runner-*, *-dev, *staging*). Enter one pattern per line. Matching is case-insensitive.', resetLabel: 'Reset', placeholder: 'runner-', }); diff --git a/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts b/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts index 5d1b19b3c..dcf24cab5 100644 --- a/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts @@ -44,7 +44,7 @@ describe('apiTokenPresentation', () => { expect(API_TOKEN_DOCKER_MANAGE_PRESET_LABEL).toBe('Docker / Podman manage'); expect(API_TOKEN_NAME_PLACEHOLDER).toBe('e.g. Docker / Podman automation'); expect(API_TOKEN_ACCESS_PANEL_DESCRIPTION).toBe( - 'Generate scoped tokens for Docker / Podman, system agents, and automation pipelines. Tokens are shown once; store them securely and rotate when infrastructure changes.', + 'Generate scoped tokens for Docker / Podman, system agents, and automation pipelines. Tokens are shown once. Store them securely and rotate when infrastructure changes.', ); expect(API_TOKEN_DOCKER_REPORT_PRESET_DESCRIPTION).toBe( 'Permits Docker / Podman agents to stream runtime and container telemetry only.', diff --git a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts index 51427f16f..5eadcb270 100644 --- a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts @@ -127,7 +127,7 @@ describe('discoveryPresentation', () => { items: [ 'Environment variables still override these settings.', 'Changes made here are saved to system.json immediately.', - 'Automatic mode can scan every detected interface, including bridge or shared networks; use custom subnets when scope matters.', + 'Automatic mode can scan every detected interface, including bridge or shared networks. Use custom subnets when scope matters.', ], }); diff --git a/frontend-modern/src/utils/actionAuditPresentation.ts b/frontend-modern/src/utils/actionAuditPresentation.ts index 115071f21..d1ffdd003 100644 --- a/frontend-modern/src/utils/actionAuditPresentation.ts +++ b/frontend-modern/src/utils/actionAuditPresentation.ts @@ -353,7 +353,7 @@ export const getActionAuditVerificationOutcomePresentation = (audit: { ] : [ 'Confirmation lacks an evidence source', - 'The record says confirmed but provides no evidence source; do not treat it as independently verified.', + 'The record says confirmed but provides no evidence source. Do not treat it as independently verified.', ], contradicted: ['Outcome contradicted', 'Observed evidence contradicted the intended state.'], inconclusive: [ diff --git a/frontend-modern/src/utils/aiFindingPresentation.ts b/frontend-modern/src/utils/aiFindingPresentation.ts index 5e4382f72..da2cfe6c5 100644 --- a/frontend-modern/src/utils/aiFindingPresentation.ts +++ b/frontend-modern/src/utils/aiFindingPresentation.ts @@ -1116,7 +1116,7 @@ export const getFindingPatrolWorkflowPresentation = ( return { stage: 'approval', label: 'Run approved action', - detail: 'The typed action is approved; execution remains a separate operator step.', + detail: 'The typed action is approved. Execution remains a separate operator step.', tone: 'success', }; } diff --git a/frontend-modern/src/utils/alertThresholdsPresentation.ts b/frontend-modern/src/utils/alertThresholdsPresentation.ts index 3de57a522..fff86863b 100644 --- a/frontend-modern/src/utils/alertThresholdsPresentation.ts +++ b/frontend-modern/src/utils/alertThresholdsPresentation.ts @@ -54,7 +54,7 @@ export const ALERT_THRESHOLDS_BACKUP_IGNORE_VMIDS_DESCRIPTION = export const ALERT_THRESHOLDS_BACKUP_IGNORE_VMIDS_PLACEHOLDER = '100, 200, 10*'; export const ALERT_THRESHOLDS_DOCKER_IGNORED_PREFIXES_TITLE = 'Ignored container patterns'; export const ALERT_THRESHOLDS_DOCKER_IGNORED_PREFIXES_DESCRIPTION = - 'Containers whose name or ID matches any pattern below are skipped for container alerts. A bare entry matches as a prefix; use * as a wildcard (runner-*, *-dev, *staging*). Enter one pattern per line; matching is case-insensitive.'; + 'Containers whose name or ID matches any pattern below are skipped for container alerts. A bare entry matches as a prefix. Use * as a wildcard (runner-*, *-dev, *staging*). Enter one pattern per line. Matching is case-insensitive.'; export const ALERT_THRESHOLDS_DOCKER_IGNORED_PREFIXES_RESET_LABEL = 'Reset'; export const ALERT_THRESHOLDS_DOCKER_IGNORED_PREFIXES_PLACEHOLDER = 'runner-'; export const ALERT_THRESHOLDS_DOCKER_SERVICES_TITLE = 'Swarm service alerts'; @@ -108,7 +108,7 @@ export function getAlertThresholdsHelpBanner() { toggleGuidance: 'Use the On/Off control beside a metric to enable or disable its alerts.', inheritanceGuidance: 'Resources inherit their group defaults until you save a custom override.', bulkGuidance: 'Select multiple resources to apply the same threshold changes in bulk.', - collapseHint: 'Resource groups start collapsed; open only the group you need.', + collapseHint: 'Resource groups start collapsed. Open only the group you need.', } as const; } diff --git a/frontend-modern/src/utils/apiTokenPresentation.ts b/frontend-modern/src/utils/apiTokenPresentation.ts index 21f4df91c..771384334 100644 --- a/frontend-modern/src/utils/apiTokenPresentation.ts +++ b/frontend-modern/src/utils/apiTokenPresentation.ts @@ -19,7 +19,7 @@ export const API_TOKEN_DOCKER_PODMAN_RUNTIMES_LABEL = `${DOCKER_PODMAN_SOURCE_LA export const API_TOKEN_DOCKER_REPORT_PRESET_LABEL = `${DOCKER_PODMAN_SOURCE_LABEL} report`; export const API_TOKEN_DOCKER_MANAGE_PRESET_LABEL = `${DOCKER_PODMAN_SOURCE_LABEL} manage`; export const API_TOKEN_NAME_PLACEHOLDER = `e.g. ${DOCKER_PODMAN_SOURCE_LABEL} automation`; -export const API_TOKEN_ACCESS_PANEL_DESCRIPTION = `Generate scoped tokens for ${DOCKER_PODMAN_SOURCE_LABEL}, system agents, and automation pipelines. Tokens are shown once; store them securely and rotate when infrastructure changes.`; +export const API_TOKEN_ACCESS_PANEL_DESCRIPTION = `Generate scoped tokens for ${DOCKER_PODMAN_SOURCE_LABEL}, system agents, and automation pipelines. Tokens are shown once. Store them securely and rotate when infrastructure changes.`; export const API_TOKEN_DOCKER_REPORT_PRESET_DESCRIPTION = `Permits ${DOCKER_PODMAN_SOURCE_LABEL} agents to stream runtime and container telemetry only.`; export const API_TOKEN_DOCKER_MANAGE_PRESET_DESCRIPTION = `Extends ${DOCKER_PODMAN_SOURCE_LABEL} reporting with lifecycle actions (restart, stop, etc.).`; diff --git a/frontend-modern/src/utils/discoveryPresentation.ts b/frontend-modern/src/utils/discoveryPresentation.ts index e0fc30948..4d41588ce 100644 --- a/frontend-modern/src/utils/discoveryPresentation.ts +++ b/frontend-modern/src/utils/discoveryPresentation.ts @@ -347,7 +347,7 @@ export function getNetworkDiscoveryPriorityNotice() { items: [ 'Environment variables still override these settings.', 'Changes made here are saved to system.json immediately.', - 'Automatic mode can scan every detected interface, including bridge or shared networks; use custom subnets when scope matters.', + 'Automatic mode can scan every detected interface, including bridge or shared networks. Use custom subnets when scope matters.', ], } as const; } diff --git a/frontend-modern/src/utils/licensePresentation.ts b/frontend-modern/src/utils/licensePresentation.ts index 79e3f9926..f892631f5 100644 --- a/frontend-modern/src/utils/licensePresentation.ts +++ b/frontend-modern/src/utils/licensePresentation.ts @@ -383,8 +383,8 @@ const getPulseProRuntimeMismatchDetail = ( entitlements?: Pick | null, ): string => getPulseProRuntimeBuild(entitlements) === 'community' - ? `This install reports the community runtime. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key (starts ppk_live_) and install the private Pulse Pro runtime; if you do not have an activation key, issue one at ${PULSE_PRO_RETRIEVE_LICENSE_URL} with your purchase email. Public GitHub releases and the public Docker image do not include Pro-only runtime hooks.` - : `This install is not reporting a Pulse Pro runtime identity. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key (starts ppk_live_) and install the private Pulse Pro runtime; if you do not have an activation key, issue one at ${PULSE_PRO_RETRIEVE_LICENSE_URL} with your purchase email. Public GitHub releases and the public Docker image do not include Pro-only runtime hooks.`; + ? `This install reports the community runtime. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key (starts ppk_live_) and install the private Pulse Pro runtime. If you do not have an activation key, issue one at ${PULSE_PRO_RETRIEVE_LICENSE_URL} with your purchase email. Public GitHub releases and the public Docker image do not include Pro-only runtime hooks.` + : `This install is not reporting a Pulse Pro runtime identity. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key (starts ppk_live_) and install the private Pulse Pro runtime. If you do not have an activation key, issue one at ${PULSE_PRO_RETRIEVE_LICENSE_URL} with your purchase email. Public GitHub releases and the public Docker image do not include Pro-only runtime hooks.`; const getPatrolControlAction = ({ entitlements, @@ -906,7 +906,7 @@ export const getSelfHostedPlanStatusPresentation = ( : `This instance has ${actualHistoryDays} days of metric history available.` : historyState === 'partial' ? `This instance reports ${actualHistoryDays} days of metric history, below the expected ${requiredHistoryDays} days.` - : `This instance does not have metric-history access yet; expected ${requiredHistoryDays} days.`, + : `This instance does not have metric-history access yet. Expected ${requiredHistoryDays} days.`, }); if (planDefinition.tier === 'pro') { @@ -1067,7 +1067,7 @@ export const getCommercialMigrationNotice = ( break; case 'exchange_connectivity_required': body = - 'Pulse has not been able to reach license.pulserelay.pro for over a day. Paid v6 features require periodic outbound HTTPS to that host. Core monitoring keeps running; paid features stay on Community until connectivity is allowed. See docs/UPGRADE_v6.md for the connectivity policy.'; + 'Pulse has not been able to reach license.pulserelay.pro for over a day. Paid v6 features require periodic outbound HTTPS to that host. Core monitoring keeps running. Paid features stay on Community until connectivity is allowed. See docs/UPGRADE_v6.md for the connectivity policy.'; break; case 'exchange_unavailable': default: diff --git a/frontend-modern/src/utils/relayPresentation.ts b/frontend-modern/src/utils/relayPresentation.ts index 502f98c22..05723c3b1 100644 --- a/frontend-modern/src/utils/relayPresentation.ts +++ b/frontend-modern/src/utils/relayPresentation.ts @@ -42,7 +42,7 @@ export const RELAY_ACTIVATION_REQUIRED_LABEL = 'Activation required'; export const RELAY_ACTIVATION_REQUIRED_MESSAGE = 'Remote Access is enabled, but this instance does not have an active Relay token. Activate a Relay-capable plan or turn Remote Access off before pairing mobile clients.'; export const RELAY_PAIRING_APP_AVAILABILITY_TEXT = - 'Pulse Mobile is in early access for iOS and Android; to get the install links, enter your activation key on'; + 'Pulse Mobile is in early access for iOS and Android. To get the install links, enter your activation key on'; export const RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL = 'your download page'; export const RELAY_PAIRING_APP_DOWNLOAD_LINK_CLASS = 'font-medium text-blue-600 underline-offset-2 hover:underline dark:text-blue-400'; diff --git a/frontend-modern/src/utils/resourceAnalysisPresentation.ts b/frontend-modern/src/utils/resourceAnalysisPresentation.ts index 8ee4333ba..63c9ce4df 100644 --- a/frontend-modern/src/utils/resourceAnalysisPresentation.ts +++ b/frontend-modern/src/utils/resourceAnalysisPresentation.ts @@ -2,7 +2,7 @@ export const RESOURCE_ANALYSIS_LABEL = 'Analysis'; export const RESOURCE_SAFE_SUMMARY_LABEL = 'Safe Summary'; export const DISCOVERY_ANALYSIS_REASONING_LABEL = 'Analysis Reasoning'; export const DISCOVERY_ANALYSIS_EXPLANATION = - 'Discovery runs read-only commands to gather system information (processes, ports, services), then uses the configured analysis provider to identify what is running. No data is stored externally; only the analysis result is saved locally.'; + 'Discovery runs read-only commands to gather system information (processes, ports, services), then uses the configured analysis provider to identify what is running. No data is stored externally. Only the analysis result is saved locally.'; export const formatResourceAnalysisSummary = (grade: string, score: number): string => `${RESOURCE_ANALYSIS_LABEL} ${grade} · ${Math.round(score)}/100`;