From 1a0cd7c9d5aa02de754535a293dc0cacd4ef6fa0 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 19:25:52 +0100
Subject: [PATCH 1/2] Keep private-file and uninstall guidance clear through
copy lint
Split two existing instructions into plain sentences without changing private-token handling, host-local removal or file ownership requirements. Verify the actual installer and removed-agent guidance and clipboard outputs in desktop Chromium and phone WebKit.
Contract-Neutral: Copy-only sentence changes preserve credential transport, installation and removal contracts.
Change-source: pulse-maintainer
---
.../browser-tests/unix-guidance-copy.cjs | 193 ++++++++++++++++++
.../browser-tests/unix-private-install.tsx | 12 +-
frontend-modern/browser-verification.json | 49 ++---
.../InfrastructureAgentDoctorPage.tsx | 2 +-
.../infrastructureOperationsModel.tsx | 2 +-
5 files changed, 225 insertions(+), 33 deletions(-)
create mode 100644 frontend-modern/browser-tests/unix-guidance-copy.cjs
diff --git a/frontend-modern/browser-tests/unix-guidance-copy.cjs b/frontend-modern/browser-tests/unix-guidance-copy.cjs
new file mode 100644
index 000000000..c074bb014
--- /dev/null
+++ b/frontend-modern/browser-tests/unix-guidance-copy.cjs
@@ -0,0 +1,193 @@
+// Scoped copy proof: actual installer/Doctor/styles, synthetic token API only.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { chromium, webkit } = require('playwright');
+(async () => {
+ const root = '/workspace/frontend-modern';
+ process.chdir(root);
+ const artifacts = path.join(root, 'node_modules', 'unix-guidance-browser');
+ fs.mkdirSync(artifacts, { recursive: true });
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ root,
+ configFile: path.join(root, 'vite.config.ts'),
+ cacheDir: path.join(root, 'node_modules', '.vite-unix-guidance-copy'),
+ optimizeDeps: {
+ noDiscovery: true,
+ include: ['solid-js', 'solid-js/web', '@solidjs/router'],
+ },
+ server: { host: '127.0.0.1', port: 5244, strictPort: true },
+ });
+ const secret = 'browser-synthetic-install-token';
+ const results = [];
+ let browser;
+ try {
+ await server.listen();
+ for (const [engine, width, tone] of [
+ ['chromium', 1280, 'light'],
+ ['webkit', 390, 'dark'],
+ ]) {
+ browser = await { chromium, webkit }[engine].launch(
+ engine === 'chromium'
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({
+ viewport: { width, height: 900 },
+ ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
+ });
+ const errors = [];
+ page.on('pageerror', (error) => {
+ errors.push(error.message);
+ console.log('PAGE_ERROR', error.message);
+ });
+ page.on('console', (message) => {
+ if (message.type() === 'error') {
+ errors.push(message.text());
+ console.log('CONSOLE_ERROR', message.text());
+ }
+ });
+ await page.addInitScript(() => {
+ window.__copies = [];
+ Object.defineProperty(navigator, 'clipboard', {
+ value: {
+ writeText: async (text) => window.__copies.push(text),
+ },
+ });
+ });
+ await page.route('**/api/**', async (route) => {
+ const pathname = new URL(route.request().url()).pathname;
+ // Do not intercept the production /src/api/*.ts module requests.
+ if (!pathname.startsWith('/api/')) return route.continue();
+ if (pathname === '/api/security/status')
+ return route.fulfill({
+ json: {
+ requiresAuth: true,
+ hasAuthentication: true,
+ apiTokenConfigured: true,
+ },
+ });
+ if (pathname === '/api/agent-install-command')
+ return route.fulfill({
+ json: {
+ token: secret,
+ record: {
+ id: 'synthetic-record',
+ name: 'Synthetic installer',
+ prefix: 'browser',
+ suffix: 'token',
+ createdAt: new Date().toISOString(),
+ },
+ },
+ });
+ if (pathname === '/api/state')
+ return route.fulfill({ json: { connectedInfrastructure: [] } });
+ return route.fulfill({ json: { data: [] } });
+ });
+ await page.goto('http://127.0.0.1:5244/browser-tests/unix-private-install.html', {
+ waitUntil: 'domcontentloaded',
+ timeout: 60_000,
+ });
+ await page
+ .getByRole('heading', { name: 'Private Unix agent installation', exact: true })
+ .waitFor({ timeout: 60_000 });
+ if (tone === 'dark')
+ await page.evaluate(() => document.documentElement.classList.add('dark'));
+ await page.getByRole('button', { name: 'Generate token', exact: true }).first().click();
+ const dialog = page.getByRole('dialog', { name: 'API token ready' });
+ await dialog.waitFor();
+ await dialog.getByRole('button', { name: 'Dismiss', exact: true }).click();
+ await dialog.waitFor({ state: 'hidden' });
+ const privateNote = page
+ .locator('span')
+ .filter({
+ hasText: /^Before running, save the separately revealed token/,
+ })
+ .first();
+ const fileText = await privateNote.innerText();
+ assert.ok(
+ fileText.includes(
+ 'This command does not prompt or delete your file. Remove it through the same trusted file path afterwards.',
+ ),
+ );
+ for (const warning of [
+ '0600',
+ '0700',
+ 'Never put the token in a GUI command field',
+ 'console or SSH instead',
+ ])
+ assert.ok(fileText.includes(warning));
+ await page
+ .getByRole('button', {
+ name: 'Copy Install from a private token file (no terminal) command',
+ exact: true,
+ })
+ .click();
+ const fileCommand = await page.evaluate(() => window.__copies.at(-1));
+ assert.ok(fileCommand.includes('/root/.config/pulse-agent/bootstrap-token'));
+ assert.ok(fileCommand.includes('--token-file'));
+ assert.ok(!fileCommand.includes('read -r'));
+ assert.ok(!fileCommand.includes(secret));
+ await privateNote.screenshot({ path: path.join(artifacts, `${engine}-private-file.png`) });
+ await page
+ .getByRole('button', { name: 'Show details for removed-fixture', exact: true })
+ .click();
+ const removalNote = page.locator('p').filter({
+ hasText:
+ 'This agent was removed from Pulse, but the agent software may still be installed on its host.',
+ });
+ const removalText = await removalNote.innerText();
+ assert.ok(
+ removalText.includes('token at its silent prompt. Never insert it into the command.'),
+ );
+ assert.ok(removalText.includes('Pulse does not run commands remotely'));
+ await page
+ .getByRole('button', {
+ name: 'Copy Linux / macOS / FreeBSD uninstall command for removed-fixture',
+ exact: true,
+ })
+ .click();
+ const removalCommand = await page.evaluate(() => window.__copies.at(-1));
+ assert.ok(removalCommand.includes('removed-agent-42'));
+ assert.ok(removalCommand.includes('--uninstall'));
+ assert.ok(!removalCommand.includes('--preflight-only'));
+ assert.ok(!removalCommand.includes(secret));
+ await removalNote.screenshot({ path: path.join(artifacts, `${engine}-removed-note.png`) });
+ assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
+ await page.screenshot({ path: path.join(artifacts, `${engine}-viewport.png`) });
+ assert.deepEqual(errors, []);
+ results.push({
+ engine,
+ version: browser.version(),
+ width,
+ tone,
+ fileText,
+ removalText,
+ privateFileCopied: true,
+ scopedRemovalCopied: true,
+ errors,
+ });
+ await browser.close();
+ browser = undefined;
+ }
+ fs.writeFileSync(
+ path.join(artifacts, 'result.json'),
+ JSON.stringify(
+ {
+ playwright: require('playwright/package.json').version,
+ results,
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(JSON.stringify({ result: 'passed', artifacts, results }));
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/unix-private-install.tsx b/frontend-modern/browser-tests/unix-private-install.tsx
index dd4ef40c6..0e6a2ec45 100644
--- a/frontend-modern/browser-tests/unix-private-install.tsx
+++ b/frontend-modern/browser-tests/unix-private-install.tsx
@@ -91,7 +91,17 @@ const Doctor = () => {
agentIdentity: { hostname: 'doctor.example', commandsEnabled: false },
},
} as InfrastructureAgentDoctorTarget;
- return ;
+ const removedTarget = {
+ ...target,
+ key: 'agent:removed-fixture',
+ connectionId: 'agent:removed-fixture',
+ displayName: 'removed-fixture',
+ status: 'removed',
+ needsCredentialRepair: false,
+ connection: undefined,
+ diagnostic: { agentId: 'removed-agent-42', hostname: 'removed.example' },
+ } as InfrastructureAgentDoctorTarget;
+ return ;
};
render(
() => (
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index 949d210a0..b6c48716f 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,27 +1,18 @@
{
"version": 1,
- "base_sha": "9189a7262e84b201a5af6649caf0f8cff7c6d144",
- "verified_at": "2026-10-01T13:06:33.056850Z",
+ "base_sha": "53e69c06fe24e3c07deaddb5e40418d07bb0b34b",
+ "verified_at": "2026-10-01T18:25:48.680085Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
- "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
- "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx",
- "frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx",
- "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx",
- "frontend-modern/src/utils/agentInstallCommand.ts"
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx"
],
"content_sha256": {
- "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "416c7627303c665f956168a32e5aaf6af4268abacdd7e93045ab26cd45732d14",
- "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "d2ff336c1e602794fd696aed3541684498adfcfeaf1f4e603e21a44865fce3bb",
- "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "983a44871cfb908851c215a177e4a39185f7ac9017cc1b2152323b7ee396995a",
- "frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx": "c57bf5a99d457d6d21f0d22e0cc34af3618db214391fd09afce34743173a1c0f",
- "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx": "276c89076d36670752f5d37be1895d739ae13501ea9d87d1cc3a497c7e6f5895",
- "frontend-modern/src/utils/agentInstallCommand.ts": "9bb98b1db1b49aa478f894b346479951954899d4c6049d991ec4c75730cafa30"
+ "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "f9b44049c02a5e5146d882befeefe8aa5bab221a2c4a0951e529c274a416010c",
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "1f04fe06cfbb1874c657883e07b4c7ce0992399696b5bed05f3ea029b1f00c0f"
},
"routes": [
- "/browser-tests/unix-private-install.html",
- "/browser-tests/unix-private-install.html?optional=1"
+ "/browser-tests/unix-private-install.html"
],
"viewports": [
{
@@ -34,29 +25,27 @@
}
],
"states": [
- "Actual infrastructure installer, operations closures, Agent Doctor and token dialog with production CSS; synthetic APIs, not installed agent/appliance execution",
- "Required-auth Unix command and credential remain separate; single-line command grammar, private file no-terminal variant and separate-token instructions",
- "Canonical repair/removal identity and custom CA preserved; saved-state update has no replacement credential; removal has no new-binary preflight",
- "Optional-auth commands require no terminal or token-file; default TLS verification and monitoring-only issuance unchanged",
- "Desktop Chromium light and phone-emulated WebKit dark; four final screenshots visually inspected"
+ "Production installer and Agent Doctor with synthetic APIs, required authentication and separately issued token; not native installation or enrollment",
+ "Private-file note preserves trusted editor/upload, root 0600 file and 0700 directory, no GUI secret and console/SSH fallback; command preserves the user-owned file",
+ "Removed agent note preserves host-local detachment, silent prompt and no token in copied command; command retains exact removed-agent identity and no new-binary preflight",
+ "Desktop Chromium light and phone-emulated WebKit dark; four affected-note screenshots inspected, outer page width contained"
],
"interactions": [
- "Generate scoped host token, copy token with keyboard, Escape, reopen existing reveal without issuing another token",
- "Copy actual Linux installer, no-terminal private-file installer and Agent Doctor repair commands; raw credential absent from each clipboard result",
- "Change endpoint and custom CA; copy actual operations-state repair/removal/saved-state update commands; identity/profile/trust checked",
- "Reload with optional auth and confirm tokenless commands"
+ "Generate separate token and dismiss token dialog",
+ "Copy the private-file installation command; verify fixed private path and token secrecy",
+ "Expand removed agent and copy its scoped uninstall command; verify token secrecy and no preflight"
],
- "command": "pulse-worker-browser frontend-modern/browser-tests/unix-private-install.cjs",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/unix-guidance-copy.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/chromium-commands.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/chromium-token.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/webkit-commands.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/webkit-token.png"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/chromium-private-file.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/chromium-removed-note.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/webkit-private-file.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/webkit-removed-note.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/result.json"
]
}
diff --git a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
index dd6ada842..2697cd82d 100644
--- a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
+++ b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
@@ -943,7 +943,7 @@ export const InfrastructureAgentDoctorPage: Component
{(entry) => (
diff --git a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
index ac4e77f97..dbc1fafc4 100644
--- a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
+++ b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
@@ -403,7 +403,7 @@ export const buildCommandsByPlatform = (
/root/.config/pulse-agent/bootstrap-token using a trusted file editor
or upload path. The file must be root-owned with mode 0600 in a
root-owned 0700 directory. Never put the token in a GUI command
- field. This command does not prompt or delete your file; remove it through the
+ field. This command does not prompt or delete your file. Remove it through the
same trusted file path afterwards. If your GUI cannot create private files, use
console or SSH instead. Bash must already be installed.
From 04d12bc16e2f8b48663350052ca2103aca2567f4 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 19:27:17 +0100
Subject: [PATCH 2/2] Restore semantic column headers in shipped documentation
tables
Extract the Windows-independent Docs repair from candidate 136d039de4a1f8f7debfb1f82d4fffd8bf610083. Add trusted scope after sanitization without expanding document-controlled attributes. Preserve table-local scrolling and verify the current eight-header plans table in desktop Chromium and phone WebKit; native Windows work remains separate.
Change-source: pulse-maintainer
---
.../subsystems/frontend-primitives.md | 12 ++
.../docs-fragment-navigation.html | 1 +
.../docs-fragment-navigation.tsx | 1 +
.../browser-tests/docs-table-headers.cjs | 124 ++++++++++++++++++
frontend-modern/browser-verification.json | 38 +++---
.../docs/__tests__/docMarkdown.test.ts | 29 ++++
.../src/features/docs/docMarkdown.ts | 4 +
7 files changed, 189 insertions(+), 20 deletions(-)
create mode 100644 frontend-modern/browser-tests/docs-table-headers.cjs
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 2eec02ad8..ba2e532c1 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -20,6 +20,18 @@
## Purpose
+### Shipped documentation table headers
+
+After sanitization, the documentation renderer assigns trusted `scope=col` to
+native `thead th` cells without expanding the attribute allowlist. Document
+roles, scope and classes remain untrusted, and body cells are not promoted.
+Table-local horizontal scrolling is preserved. Renderer regression tests cover
+trusted column scope and rejected author-supplied attributes. The production
+Docs/router/styles fixture checks all eight current plan-table columnheader
+roles in desktop Chromium and phone WebKit, including local scrolling and
+page-width containment. This establishes browser roles, not screen-reader
+speech or release availability.
+
### Shipped documentation fragment navigation
The shared documentation renderer assigns GitHub-compatible, document-local
diff --git a/frontend-modern/browser-tests/docs-fragment-navigation.html b/frontend-modern/browser-tests/docs-fragment-navigation.html
index db161db13..29edc2c5d 100644
--- a/frontend-modern/browser-tests/docs-fragment-navigation.html
+++ b/frontend-modern/browser-tests/docs-fragment-navigation.html
@@ -2,6 +2,7 @@
+
diff --git a/frontend-modern/browser-tests/docs-fragment-navigation.tsx b/frontend-modern/browser-tests/docs-fragment-navigation.tsx
index 5a9d65b9f..5c11587d7 100644
--- a/frontend-modern/browser-tests/docs-fragment-navigation.tsx
+++ b/frontend-modern/browser-tests/docs-fragment-navigation.tsx
@@ -16,6 +16,7 @@ const scenario = new URLSearchParams(window.location.search).get('scenario') ??
const targets: Record = {
plain: '/docs/API',
+ plans: '/docs/PULSE_PRO',
direct: '/docs/API#resource-maintenance-and-operator-state',
reload: '/docs/API#resource-maintenance-and-operator-state',
malformed: '/docs/API#%invalid',
diff --git a/frontend-modern/browser-tests/docs-table-headers.cjs b/frontend-modern/browser-tests/docs-table-headers.cjs
new file mode 100644
index 000000000..e7ba98eab
--- /dev/null
+++ b/frontend-modern/browser-tests/docs-table-headers.cjs
@@ -0,0 +1,124 @@
+// Production Docs/router/styles and shipped plan Markdown, no backend.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { chromium, webkit } = require('playwright');
+(async () => {
+ const observe = process.argv.includes('observe');
+ const root = '/workspace/frontend-modern';
+ process.chdir(root);
+ const artifacts = path.join(root, 'node_modules', 'docs-header-browser');
+ fs.mkdirSync(artifacts, { recursive: true });
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ root,
+ configFile: path.join(root, 'vite.config.ts'),
+ cacheDir: path.join(root, 'node_modules', '.vite-docs-table-headers'),
+ server: { host: '127.0.0.1', port: 5243, strictPort: true },
+ optimizeDeps: { noDiscovery: true, include: [] },
+ });
+ let browser;
+ const results = [];
+ try {
+ await server.listen();
+ for (const [engine, width, tone] of [
+ ['chromium', 1280, 'light'],
+ ['webkit', 390, 'dark'],
+ ]) {
+ browser = await { chromium, webkit }[engine].launch(
+ engine === 'chromium'
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({
+ viewport: { width, height: 900 },
+ ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
+ });
+ const errors = [];
+ page.on('response', (response) => {
+ if (response.status() >= 400)
+ console.log('FAILED_RESPONSE', response.status(), response.url());
+ });
+ page.on('pageerror', (e) => errors.push(e.message));
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ console.log('CONSOLE_ERROR', m.text(), m.location());
+ errors.push(m.text());
+ }
+ });
+ await page.goto(
+ 'http://127.0.0.1:5243/browser-tests/docs-fragment-navigation.html?scenario=plans',
+ { waitUntil: 'domcontentloaded', timeout: 60_000 },
+ );
+ await page.locator('article table').first().waitFor();
+ if (tone === 'dark')
+ await page.evaluate(() => document.documentElement.classList.add('dark'));
+ const table = page.locator('article table').filter({ hasText: 'Relay (legacy)' }).first();
+ const th = await table.locator('thead th').allTextContents();
+ const headers = await table.getByRole('columnheader').allTextContents();
+ const snapshot = await table.ariaSnapshot();
+ const scopes = await table
+ .locator('thead th')
+ .evaluateAll((nodes) => nodes.map((n) => n.getAttribute('scope')));
+ const scroll = await table.evaluate((t) => ({
+ wrapper: t.parentElement.dataset.docTableScroll !== undefined,
+ outer: document.documentElement.scrollWidth,
+ viewport: window.innerWidth,
+ width: t.parentElement.clientWidth,
+ content: t.parentElement.scrollWidth,
+ }));
+ if (!observe) {
+ assert.equal(th.length, 8);
+ assert.deepEqual(headers, th);
+ assert.deepEqual(scopes, Array(th.length).fill('col'));
+ assert.ok(snapshot.includes('columnheader "Relay (legacy)"'));
+ assert.ok(scroll.wrapper);
+ assert.ok(scroll.outer <= scroll.viewport + 1);
+ if (width === 390) {
+ assert.ok(scroll.content > scroll.width);
+ const moved = await table.evaluate((t) => {
+ t.parentElement.scrollLeft = 180;
+ return t.parentElement.scrollLeft;
+ });
+ assert.ok(moved > 0);
+ }
+ assert.deepEqual(errors, []);
+ }
+ await table.locator('thead').scrollIntoViewIfNeeded();
+ await page.screenshot({ path: path.join(artifacts, `${engine}-headers.png`) });
+ await page.screenshot({
+ path: path.join(artifacts, `${engine}-${observe ? 'before' : 'after'}.png`),
+ fullPage: true,
+ });
+ results.push({
+ engine,
+ version: browser.version(),
+ width,
+ tone,
+ th,
+ headers,
+ scopes,
+ scroll,
+ snapshot,
+ errors,
+ });
+ await browser.close();
+ browser = undefined;
+ }
+ fs.writeFileSync(
+ path.join(artifacts, observe ? 'before.json' : 'after.json'),
+ JSON.stringify(
+ { playwright: require('playwright/package.json').version, observe, results },
+ null,
+ 2,
+ ),
+ );
+ console.log(JSON.stringify({ result: observe ? 'observed' : 'passed', results, artifacts }));
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((e) => {
+ console.error(e);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index b6c48716f..45fb502a2 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,18 +1,17 @@
{
"version": 1,
- "base_sha": "53e69c06fe24e3c07deaddb5e40418d07bb0b34b",
- "verified_at": "2026-10-01T18:25:48.680085Z",
+ "base_sha": "1a0cd7c9d5aa02de754535a293dc0cacd4ef6fa0",
+ "verified_at": "2026-10-01T18:27:14.912695Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
- "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx"
+ "frontend-modern/src/features/docs/docMarkdown.ts"
],
"content_sha256": {
- "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "f9b44049c02a5e5146d882befeefe8aa5bab221a2c4a0951e529c274a416010c",
- "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "1f04fe06cfbb1874c657883e07b4c7ce0992399696b5bed05f3ea029b1f00c0f"
+ "frontend-modern/src/features/docs/docMarkdown.ts": "572a3b4bc5bc1114adef5aa64bb0ea833c0dbe5865bbd57d3dc767fd1965aa55"
},
"routes": [
- "/browser-tests/unix-private-install.html"
+ "/docs/PULSE_PRO",
+ "/browser-tests/docs-fragment-navigation.html?scenario=plans"
],
"viewports": [
{
@@ -25,27 +24,26 @@
}
],
"states": [
- "Production installer and Agent Doctor with synthetic APIs, required authentication and separately issued token; not native installation or enrollment",
- "Private-file note preserves trusted editor/upload, root 0600 file and 0700 directory, no GUI secret and console/SSH fallback; command preserves the user-owned file",
- "Removed agent note preserves host-local detachment, silent prompt and no token in copied command; command retains exact removed-agent identity and no new-binary preflight",
- "Desktop Chromium light and phone-emulated WebKit dark; four affected-note screenshots inspected, outer page width contained"
+ "Production Docs page/router/styles with the current shipped eight-column plan comparison, not a replacement table fixture",
+ "Every sanitized thead TH has trusted scope=col and all eight native columnheader roles; document role/scope/class remain stripped",
+ "Desktop Chromium light and phone-emulated WebKit dark; table-local horizontal scrolling and outer-page containment; four final full-page/header screenshots inspected",
+ "This establishes browser accessibility roles, not screen-reader speech or published availability"
],
"interactions": [
- "Generate separate token and dismiss token dialog",
- "Copy the private-file installation command; verify fixed private path and token secrecy",
- "Expand removed agent and copy its scoped uninstall command; verify token secrecy and no preflight"
+ "Navigate to the real plans document and inspect table DOM and accessibility snapshot",
+ "On phone, scroll the table horizontally without moving the outer page; bring its header into view"
],
- "command": "pulse-worker-browser frontend-modern/browser-tests/unix-guidance-copy.cjs",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/docs-table-headers.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/chromium-private-file.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/chromium-removed-note.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/webkit-private-file.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/webkit-removed-note.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/unix/result.json"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/chromium-after.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/chromium-headers.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/webkit-after.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/webkit-headers.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/after.json"
]
}
diff --git a/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts b/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts
index 56b5aaed0..45610a272 100644
--- a/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts
+++ b/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts
@@ -238,3 +238,32 @@ describe('extractDocTitle', () => {
expect(extractDocTitle('Intro\n\n# Later heading', 'FAQ')).toBe('FAQ');
});
});
+
+describe('documentation table column semantics', () => {
+ it('assigns column scope to every sanitized Markdown header without promoting data cells', () => {
+ const container = document.createElement('div');
+ container.innerHTML = renderDocMarkdown(
+ '| Feature | Community | Pro |\n| --- | --- | --- |\n| Metrics | Yes | Yes |',
+ 'PULSE_PRO',
+ );
+ const headers = Array.from(container.querySelectorAll('thead th'));
+ expect(headers).toHaveLength(3);
+ expect(headers.map((header) => header.getAttribute('scope'))).toEqual(['col', 'col', 'col']);
+ expect(container.querySelectorAll('tbody [scope]')).toHaveLength(0);
+ expect(
+ container.querySelector('table')?.parentElement?.hasAttribute('data-doc-table-scroll'),
+ ).toBe(true);
+ });
+
+ it('does not trust document-supplied roles or scope while assigning trusted column scope', () => {
+ const container = document.createElement('div');
+ container.innerHTML = renderDocMarkdown(
+ '',
+ 'PULSE_PRO',
+ );
+ const header = container.querySelector('thead th');
+ expect(header?.getAttribute('scope')).toBe('col');
+ expect(container.querySelector('[role], [onclick], th[class]')).toBeNull();
+ expect(container.querySelector('tbody th')?.hasAttribute('scope')).toBe(false);
+ });
+});
diff --git a/frontend-modern/src/features/docs/docMarkdown.ts b/frontend-modern/src/features/docs/docMarkdown.ts
index 5938c20eb..8402db43a 100644
--- a/frontend-modern/src/features/docs/docMarkdown.ts
+++ b/frontend-modern/src/features/docs/docMarkdown.ts
@@ -130,6 +130,10 @@ export function renderDocMarkdown(markdown: string, currentDocPath: string): str
*/
export function wrapTables(container: HTMLElement): void {
container.querySelectorAll('table').forEach((table) => {
+ // Sanitised Markdown has no author-controlled roles/scope. Assign trusted
+ // column semantics to header cells: browsers otherwise expose these THs
+ // as ordinary cells, leaving readers without table header associations.
+ table.querySelectorAll('thead th').forEach((header) => header.setAttribute('scope', 'col'));
const parent = table.parentElement;
if (parent?.dataset?.docTableScroll !== undefined) return;
const wrapper = window.document.createElement('div');