style(frontend): remove semicolons from product copy

Contract-Neutral: Customer-facing punctuation cleanup only; no API, lifecycle, policy, or subsystem behavior changed. The owning frontend copy contract and lint enforcement are updated.
This commit is contained in:
rcourtman
2026-08-18 10:52:38 +01:00
parent fe9c64a4d3
commit b396c93785
91 changed files with 460 additions and 236 deletions
@@ -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
+121 -34
View File
@@ -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"
]
}
}
+2 -1
View File
@@ -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",
@@ -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.');
@@ -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({
@@ -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,
@@ -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.',
},
},
{
@@ -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
@@ -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,
@@ -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,
});
});
@@ -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,
});
});
@@ -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.',
}),
}),
]);
@@ -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);
}
};
@@ -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<AIChatProps> = (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 () => {
@@ -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',
]),
@@ -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%');
});
@@ -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%');
});
});
@@ -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.',
@@ -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))
@@ -550,7 +550,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
<textarea
value={note()}
onInput={(e) => 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}
@@ -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,
@@ -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';
};
@@ -457,7 +457,7 @@ export const AgentIntegrationsPanel: Component = () => {
<p class="text-xs text-muted">
These values come from{' '}
<code class="font-mono">/api/agent/capabilities</code>. 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.
</p>
<Show when={workflowPrompts().length > 0}>
<details class="rounded-md border border-border bg-surface-alt/40 p-3">
@@ -528,7 +528,7 @@ export const AgentIntegrationsPanel: Component = () => {
)}
</For>
. 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.
</p>
</Show>
<Show when={errorCodeSummaries().length > 0}>
@@ -653,7 +653,7 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (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.'
@@ -31,7 +31,7 @@ export interface TrueNASCredentialSlotProps {
const describeTransport = (transport: NonNullable<TrueNASConnection['transport']>): 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<TrueNASCredentialSlotProps> = (pro
<Show when={props.deleteConfirming}>
<div class="rounded-md border border-border bg-surface-alt px-4 py-3 text-xs text-muted">
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.
</div>
</Show>
@@ -255,7 +255,7 @@ export const VMwareCredentialSlot: Component<VMwareCredentialSlotProps> = (props
<Show when={props.deleteConfirming}>
<div class="rounded-md border border-border bg-surface-alt px-4 py-3 text-xs text-muted">
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.
</div>
</Show>
@@ -191,7 +191,7 @@ export const DiagnosticsResultsPanel: Component<DiagnosticsResultsPanelProps> =
>
{(probe) => (
<div class="mt-0.5 text-xs text-muted">
Monitor: {pbs.state || (pbs.connected ? 'active' : 'unreachable')}; live
Monitor: {pbs.state || (pbs.connected ? 'active' : 'unreachable')}. Live
check: {probe().connected ? 'connected' : 'failed'}
</div>
)}
@@ -314,7 +314,7 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
<p class="text-xs leading-5 text-blue-800 dark:text-blue-200">
{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.`}
</p>
</div>
<div class="flex flex-col gap-2 sm:flex-row">
@@ -290,7 +290,7 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
Install the agent on the Proxmox node, not inside every LXC. In advanced options,
select <span class="font-medium">Proxmox VE node</span> and enable{' '}
<span class="font-medium">Pulse command execution</span>. Then start Pulse with{' '}
<code>PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true</code>; optionally restrict it
<code>PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true</code>. Optionally restrict it
with <code>PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS=101,102</code>. Pulse uses
bounded <code>pct exec</code> inventory for <code>docker ps</code> and{' '}
<code>docker stats</code>, skips guests that already have their own agent, and does
@@ -342,7 +342,7 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
<span class="mr-2 mt-0.5 text-emerald-500"></span>
<span>
Revoke the token any time from{' '}
<span class="font-medium">Settings Infrastructure Tokens</span>; the agent
<span class="font-medium">Settings Infrastructure Tokens</span>. The agent
stops reporting immediately.
</span>
</li>
@@ -586,7 +586,7 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
onChange={(event) => 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)
</label>
<label
@@ -118,7 +118,7 @@ export const NodeModalClusterMembersSection: Component<NodeModalClusterMembersSe
</div>
<p class={formHelpText}>
Leave a display name blank to use the native Proxmox name. Connection addresses are also
optional; enter an IP or hostname, with a port if it isn't 8006, only when Pulse cannot
optional. Enter an IP or hostname, with a port if it isn't 8006, only when Pulse cannot
reach the discovered address.
</p>
</div>
@@ -157,7 +157,7 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
<div>
<p class="font-medium text-base-content">Monitor physical disk health (SMART)</p>
<p class="mt-1 text-xs text-muted">
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.
</p>
</div>
<TogglePrimitive
@@ -146,7 +146,7 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
<p class="text-xs text-blue-800 dark:text-blue-200">
<strong>Docker inside Proxmox LXCs:</strong> 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{' '}
<code class="break-all rounded bg-blue-100 px-1 font-mono dark:bg-blue-900">
PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true
@@ -283,7 +283,7 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
await state.copyQuickSetupCommand(
'pve',
true,
'Command copied to clipboard! Run it on the server; the one-time setup token is already embedded.',
'Command copied to clipboard! Run it on the server. The one-time setup token is already embedded.',
);
}}
class="absolute top-2 right-2 p-1.5 text-slate-400 hover:text-slate-200 bg-surface-hover rounded-md transition-colors"
@@ -754,7 +754,7 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
await state.copyQuickSetupCommand(
'pbs',
false,
'Command copied to clipboard! Run it on the server; the one-time setup token is already embedded.',
'Command copied to clipboard! Run it on the server. The one-time setup token is already embedded.',
);
}}
class="absolute top-2 right-2 p-1.5 text-slate-400 hover:text-slate-200 bg-surface-hover rounded-md transition-colors"
@@ -108,7 +108,7 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
<Show when={props.deleteConfirming}>
<div class="mx-6 mb-2 rounded-md border border-border bg-surface-alt px-4 py-3 text-xs text-muted">
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.
</div>
</Show>
@@ -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();
@@ -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();
});
});
@@ -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');
@@ -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"', () => {
@@ -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.',
);
});
});
@@ -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',
);
});
@@ -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) {
@@ -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 => {
@@ -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: (
<span>
Command auto-escalates with <code>sudo</code> when available; otherwise run from a root
Command auto-escalates with <code>sudo</code> when available. Otherwise run from a root
shell (for example <code>su -</code>). Auto-detects your init system and works on
Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more.
</span>
@@ -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}`
@@ -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();
@@ -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';
@@ -251,7 +251,7 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
>
{verificationStatus() === 'verified'
? 'Outcome verified'
: 'Execution finished; verification was not conclusive'}
: 'Execution finished. Verification was not conclusive'}
</div>
</Show>
<Show when={currentAction().state === 'failed'}>
@@ -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);
}
+1 -1
View File
@@ -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)',
@@ -351,7 +351,7 @@ export const ActionDecisionPacket: Component<{
: 'Pulse recorded the delivery attempt before sending it.'}
</p>
<p class="mt-1 text-sm text-muted">
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.
</p>
<dl class="mt-3 grid gap-3 text-sm sm:grid-cols-2">
@@ -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(' ');
}
@@ -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(
@@ -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');
@@ -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',
},
];
};
@@ -411,7 +411,7 @@ export function AlertIntentPolicyPanel(props: {
placeholder="Use inherited behavior"
/>
<span class={formHelpText}>
Blank inherits the existing policy; 0 alerts on the first stopped observation.
Blank inherits the existing policy. 0 alerts on the first stopped observation.
</span>
</label>
<label class={formField}>
@@ -539,7 +539,7 @@ export function AlertIntentPolicyPanel(props: {
onInput={(event) => setOverrideGrace(event.currentTarget.value)}
placeholder="Inherit"
/>
<span class={formHelpText}>Leave blank to inherit; 0 means no wait.</span>
<span class={formHelpText}>Leave blank to inherit. 0 means no wait.</span>
</label>
<FormSelect
label="Operator state override"
@@ -149,7 +149,7 @@ export const DockerContainerLifecycleControls: Component<DockerContainerLifecycl
const containerName = dockerContainerLifecycleName(props.resource);
if (disabledReason) return `${label} unavailable: ${disabledReason}`;
if (planningAction() === action) return `Preparing review for ${action} ${containerName}`;
if (lastError()) return `${label} ${containerName}; last error: ${lastError()}`;
if (lastError()) return `${label} ${containerName}. Last error: ${lastError()}`;
return `Review ${action} for ${containerName}`;
};
@@ -28,7 +28,7 @@ export function DockerIdentityConflictNotice(props: DockerIdentityConflictNotice
`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 VMs with the same /etc/machine-id, so their reports overwrite each other. ` +
@@ -928,7 +928,7 @@ describe('Docker native tables', () => {
);
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();
});
@@ -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', () => {
@@ -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(
@@ -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}`;
@@ -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'}.
</div>
</Show>
@@ -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: {
@@ -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',
@@ -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',
@@ -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.',
);
});
@@ -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',
);
});
@@ -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.',
});
});
});
@@ -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,
},
];
@@ -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 {
@@ -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. ` +
@@ -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%',
);
});
});
@@ -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,
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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':
+7 -7
View File
@@ -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':
+2 -2
View File
@@ -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':
@@ -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();
+4 -4
View File
@@ -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' }));
}
@@ -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',
@@ -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',
});
});
@@ -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-',
});
@@ -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.',
@@ -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.',
],
});
@@ -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: [
@@ -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',
};
}
@@ -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;
}
@@ -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.).`;
@@ -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;
}
@@ -383,8 +383,8 @@ const getPulseProRuntimeMismatchDetail = (
entitlements?: Pick<LicenseCommercialEntitlements, 'runtime'> | 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:
@@ -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';
@@ -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`;