diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index c3bac1b5c9..560a03e88d 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -401,9 +401,20 @@ priority.
Expandable platform summary rows use
`getPlatformResourceDetailRowInteractionProps` (or
-`createPlatformResourceDetailState`, which owns the same state contract) for
-whole-row pointer activation, Enter/Space keyboard activation, focus treatment,
-`aria-expanded` / `aria-controls`, and exclusion of embedded links and controls.
+`createPlatformResourceDetailState` for their expansion state) for whole-row
+pointer activation and exclusion of embedded links and controls. The nested
+native disclosure button owns keyboard activation, focus and
+`aria-expanded` / `aria-controls`; static table rows are not duplicate controls.
+The shared `TableRow` installs a non-activating native click target only while
+an `onClick` action exists. WebKit touch must be able to synthesize that row's
+click, but actions remain document-delegated so embedded controls can stop
+propagation before the row action runs. Do not move the action into the native
+compatibility listener or add touch-end activation that opens rows during
+scrolling. Explicit caller-owned native listeners remain caller-owned.
+`Table.test.tsx` covers native binding, removal, single activation, child
+isolation and bound handlers; `PulseDataGrid.test.tsx` checks inheritance.
+The production PBS table/drawer browser fixture exercises first-tap disclosure
+and History in WebKit phone emulation, not installed collection or devices.
`PlatformResourceDetailToggleButton` is the desktop disclosure affordance and
is visually removed on phone layouts where the complete row is the touch target;
provider tables must not add a second mobile chevron. When row activation
diff --git a/frontend-modern/browser-tests/table-row-touch.cjs b/frontend-modern/browser-tests/table-row-touch.cjs
new file mode 100644
index 0000000000..025a5cd043
--- /dev/null
+++ b/frontend-modern/browser-tests/table-row-touch.cjs
@@ -0,0 +1,245 @@
+// Real production table rows and PBS drawers; synthetic bounded HTTP data.
+// Do not instrument rows with native listeners: doing so masks the WebKit bug.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { createHash } = require('node:crypto');
+const { chromium, firefox, webkit } = require('playwright');
+
+(async () => {
+ const root = '/workspace/frontend-modern';
+ process.chdir(root);
+ const engine = process.argv.find((arg) => arg.startsWith('--engine='))?.split('=')[1] || 'webkit';
+ const theme = process.argv.includes('--dark') ? 'dark' : 'light';
+ const width = process.argv.includes('--desktop') ? 1365 : 390;
+ assert.ok(['chromium', 'firefox', 'webkit'].includes(engine));
+ const out = path.join(root, 'node_modules', `table-row-touch-${engine}-${width}-${theme}`);
+ fs.mkdirSync(out, { recursive: true });
+ fs.chmodSync(out, 0o755);
+ 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: fs.mkdtempSync(path.join(out, 'vite-cache-')),
+ server: { host: '127.0.0.1', port: 5227, strictPort: true },
+ });
+ const sourceHashes = Object.fromEntries(
+ ['src/components/shared/Table.tsx', 'package-lock.json'].map((file) => [
+ file,
+ createHash('sha256')
+ .update(fs.readFileSync(path.join(root, file)))
+ .digest('hex'),
+ ]),
+ );
+ const playwrightVersion = require('playwright/package.json').version;
+ assert.equal(
+ playwrightVersion,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ let browser, page;
+ const errors = [],
+ requests = [],
+ observations = [];
+ const snapshot = async (name) => {
+ const file = path.join(out, name + '.png');
+ await page.screenshot({ path: file });
+ fs.chmodSync(file, 0o644);
+ };
+ const activate = (locator) => (width === 390 ? locator.tap() : locator.click());
+ const noOverflow = async () =>
+ assert.ok(
+ await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
+ 'document overflow',
+ );
+ try {
+ await server.listen();
+ browser = await { chromium, firefox, webkit }[engine].launch(
+ engine === 'chromium'
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ page = await browser.newPage({
+ viewport: { width, height: width === 390 ? 844 : 900 },
+ locale: 'en-GB',
+ timezoneId: 'UTC',
+ ...(width === 390 ? { isMobile: true, hasTouch: true } : {}),
+ });
+ page.setDefaultTimeout(20_000);
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.route('**/*', async (route) => {
+ const url = new URL(route.request().url());
+ if (url.origin !== 'http://127.0.0.1:5227') return route.abort();
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ requests.push({ path: url.pathname, search: url.search, method: route.request().method() });
+ assert.equal(route.request().method(), 'GET');
+ if (url.pathname === '/api/license/runtime-capabilities')
+ return route.fulfill({
+ json: {
+ capabilities: [],
+ limits: [],
+ max_history_days: 7,
+ hosted_mode: false,
+ runtime: { build: 'community', label: 'Pulse Community runtime' },
+ blocked_capabilities: [],
+ },
+ });
+ if (url.pathname === '/api/metrics-store/history') {
+ const id = url.searchParams.get('resourceId');
+ assert.ok(['vm-one', 'vm-two', 'agent-three'].includes(id), `unexpected target ${id}`);
+ const end = Date.UTC(2026, 9, 1, 12);
+ return route.fulfill({
+ json: {
+ resourceType: url.searchParams.get('resourceType'),
+ resourceId: id,
+ range: url.searchParams.get('range'),
+ start: end - 86400000,
+ end,
+ source: 'store',
+ metrics: {
+ cpu: [
+ { timestamp: end - 600000, value: 20 },
+ { timestamp: end - 300000, value: 30 },
+ ],
+ memory: [{ timestamp: end - 300000, value: 40 }],
+ netin: [
+ { timestamp: end - 600000, value: 1024 },
+ { timestamp: end - 300000, value: 2048 },
+ ],
+ },
+ },
+ });
+ }
+ return route.fulfill({ json: { data: [], enabled: false } });
+ });
+ console.log(JSON.stringify({ stage: 'production-pbs', engine, theme, width, sourceHashes }));
+ await page.goto('http://127.0.0.1:5227/browser-tests/pbs-identity-boundary.html', {
+ waitUntil: 'domcontentloaded',
+ timeout: 120_000,
+ });
+ await page.evaluate(
+ (dark) => document.documentElement.classList.toggle('dark', dark),
+ theme === 'dark',
+ );
+ await activate(page.getByRole('button', { name: 'Corroborate links', exact: true }));
+ for (const suffix of ['one', 'two', 'three']) {
+ const cell = page.locator(`td[title="backup-connection-${suffix} · tank"]`);
+ const disclosure = cell.getByRole('button');
+ assert.equal(await disclosure.getAttribute('aria-expanded'), 'false');
+ // No focus, force, dispatchEvent, synthetic click or added event listeners.
+ await activate(cell);
+ await page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`).waitFor();
+ assert.equal(await disclosure.getAttribute('aria-expanded'), 'true');
+ const detail = page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`);
+ observations.push({ suffix, firstActivationOpened: true });
+ await noOverflow();
+ if (suffix === 'one') await snapshot('pbs-first-activation');
+ await activate(detail.getByRole('tab', { name: 'History', exact: true }));
+ await detail.getByTestId('guest-history-plot').first().locator('path').first().waitFor();
+ assert.equal(
+ await disclosure.getAttribute('aria-expanded'),
+ 'true',
+ 'tab must not retoggle row',
+ );
+ if (suffix === 'three') await snapshot('pbs-third-history');
+ // Close by the same ordinary cell; nested History input cannot consume it.
+ await activate(cell);
+ assert.equal(await disclosure.getAttribute('aria-expanded'), 'false');
+ assert.equal(await detail.count(), 0);
+ }
+ const keyboardCell = page.locator('td[title="backup-connection-one · tank"]');
+ const keyboardDisclosure = keyboardCell.getByRole('button');
+ await keyboardDisclosure.focus();
+ await page.keyboard.press('Enter');
+ assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'true');
+ await page.keyboard.press('Space');
+ assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'false');
+ assert.ok(requests.some((request) => request.search.includes('resourceId=vm-one')));
+ assert.ok(requests.some((request) => request.search.includes('resourceId=vm-two')));
+ assert.ok(requests.some((request) => request.search.includes('resourceId=agent-three')));
+
+ console.log(JSON.stringify({ stage: 'shared-controls', engine, theme, width }));
+ await page.goto('http://127.0.0.1:5227/browser-tests/table-row-touch.html', {
+ waitUntil: 'domcontentloaded',
+ timeout: 120_000,
+ });
+ await page.evaluate(
+ (dark) => document.documentElement.classList.toggle('dark', dark),
+ theme === 'dark',
+ );
+ const state = async () =>
+ JSON.parse(await page.getByRole('status', { name: 'Action state' }).innerText());
+ const name = page.getByText('Shared resource name', { exact: true });
+ await name.waitFor();
+ await activate(name);
+ assert.equal((await state()).rowActions, 1);
+ assert.equal((await state()).expanded, true);
+ await activate(page.getByRole('button', { name: 'Child action', exact: true }));
+ await activate(page.getByRole('link', { name: 'Native link' }));
+ assert.equal(new URL(page.url()).hash, '#native-link');
+ await activate(page.getByRole('checkbox', { name: 'Child checkbox' }));
+ assert.equal(await page.getByRole('checkbox').isChecked(), true);
+ assert.equal((await state()).rowActions, 1, 'nested controls must not run row action');
+ assert.equal((await state()).childActions, 2);
+ const toggle = page.getByTestId('control-row').locator('[data-row-action="true"]');
+ await activate(toggle);
+ assert.equal((await state()).rowActions, 2, 'nested disclosure must toggle once');
+ await toggle.focus();
+ await page.keyboard.press('Enter');
+ assert.equal((await state()).rowActions, 3);
+ await page.keyboard.press('Space');
+ assert.equal((await state()).rowActions, 4);
+ await activate(page.getByRole('button', { name: 'Toggle row action' }));
+ await activate(name);
+ assert.equal((await state()).rowActions, 4);
+ await activate(page.getByRole('button', { name: 'Toggle row action' }));
+ await activate(name);
+ assert.equal((await state()).rowActions, 5);
+ await activate(page.getByText('Static row', { exact: true }));
+ assert.equal((await state()).rowActions, 5);
+ await activate(page.getByText('Grid resource', { exact: true }));
+ assert.equal((await state()).gridActions, 1);
+ await activate(page.getByRole('button', { name: 'Grid child action', exact: true }));
+ assert.equal((await state()).gridActions, 1);
+ assert.equal((await state()).childActions, 3);
+ await noOverflow();
+ await snapshot('shared-controls');
+ assert.deepEqual(errors, []);
+ const result = {
+ engine,
+ browserVersion: browser.version(),
+ playwrightVersion,
+ theme,
+ width,
+ sourceHashes,
+ observations,
+ requests,
+ controlState: await state(),
+ errors,
+ result: 'passed',
+ };
+ fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify(result, null, 2) + '\n', {
+ mode: 0o644,
+ });
+ console.log(JSON.stringify(result));
+ } catch (error) {
+ if (page) await snapshot('failure').catch(() => {});
+ fs.writeFileSync(
+ path.join(out, 'failure.json'),
+ JSON.stringify(
+ { error: String(error), errors, requests, observations, sourceHashes },
+ null,
+ 2,
+ ) + '\n',
+ { mode: 0o644 },
+ );
+ throw error;
+ } finally {
+ await browser?.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/table-row-touch.html b/frontend-modern/browser-tests/table-row-touch.html
new file mode 100644
index 0000000000..9fd7276bf5
--- /dev/null
+++ b/frontend-modern/browser-tests/table-row-touch.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Shared table touch verification
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/table-row-touch.tsx b/frontend-modern/browser-tests/table-row-touch.tsx
new file mode 100644
index 0000000000..ab8e033077
--- /dev/null
+++ b/frontend-modern/browser-tests/table-row-touch.tsx
@@ -0,0 +1,91 @@
+// Real shared table/grid actions with synthetic data, not an installed journey.
+import { createSignal } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Table, TableBody, TableCell, TableRow } from '../src/components/shared/Table';
+import { SummaryRowActionButton } from '../src/components/shared/SummaryRowActionButton';
+import { PulseDataGrid } from '../src/components/shared/PulseDataGrid';
+import '../src/index.css';
+
+function Fixture() {
+ const [expanded, setExpanded] = createSignal(false);
+ const [rowActions, setRowActions] = createSignal(0);
+ const [childActions, setChildActions] = createSignal(0);
+ const [enabled, setEnabled] = createSignal(true);
+ const [gridActions, setGridActions] = createSignal(0);
+ const toggle = () => {
+ setRowActions((count) => count + 1);
+ setExpanded((value) => !value);
+ };
+ const child = (event: MouseEvent) => {
+ event.stopPropagation();
+ setChildActions((count) => count + 1);
+ };
+ return (
+
+ Shared table touch verification
+
+
+
+
+ data={[{ id: 'grid-one', name: 'Grid resource' }]}
+ keyExtractor={(row) => row.id}
+ onRowClick={() => setGridActions((count) => count + 1)}
+ columns={[
+ { key: 'name', label: 'Name' },
+ {
+ key: 'action',
+ label: 'Action',
+ render: () => (
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
+render(() => , document.getElementById('root')!);
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index 6fd5437339..51ed3966f6 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,124 +1,189 @@
{
"version": 1,
- "base_sha": "00f82e2afc3cd75035c997e9e245c6e0eeffffa9",
- "verified_at": "2026-10-01T01:42:43Z",
+ "base_sha": "920aa2f27c6cb717cca7eb8e2cd3f1379d0fdf73",
+ "verified_at": "2026-10-01T03:23:21Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx",
- "frontend-modern/src/components/Workloads/guestDrawerModel.ts"
+ "frontend-modern/src/components/shared/Table.tsx"
],
"content_sha256": {
- "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "a49f3c6496e5515284a94c52b818f78f72d2e3530ae398794ea7432f251f48ec",
- "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5ea99d3d995ddb2fa777273ed3f224b7fbb9e7b10a015bed52d61a8e10dc0b99"
+ "frontend-modern/src/components/shared/Table.tsx": "4009a751a3fa435cb4067a98989d25cac4e9e6d08acd7b91701fc98fbfd5aaf1"
},
"routes": [
- "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer, shared History/query and CSS with synthetic VM/Agent APIs; not installed collection or complete #1723 acceptance)"
+ "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer and History with synthetic bounded API/Agent/PVE data)",
+ "/browser-tests/table-row-touch.html (production Table, SummaryRowActionButton and PulseDataGrid with synthetic rows and delegated embedded actions)"
],
"viewports": [
- {
- "width": 1365,
- "height": 900
- },
{
"width": 390,
"height": 844
+ },
+ {
+ "width": 1365,
+ "height": 900
}
],
"states": [
- "light/dark short sample coverage stays at its true position in the fulfilled 24-hour window rather than stretching across each plot",
- "all configured utilization/network/disk/thermal panels share dated endpoints, including lone and empty panels",
- "three separately mapped synthetic PBS drawers read vm-one, vm-two and agent-three, not unrelated host identities",
- "same-time observations occupy the same horizontal coordinate across groups with different sample coverage",
- "failed same-source refresh retains samples and window with a sanitised warning; successful refresh advances the window",
- "held 1-hour range replacement clears former endpoints, paths and inspection controls; matching data then uses the new window",
- "empty window has labelled bounds without a synthetic trend; seven-day bounds remain dated; locked fourteen-day range makes no read"
+ "WebKit phone light/dark, Chromium phone light and Firefox desktop dark: all three synthetic PBS drawers open with the first ordinary cell activation and close with the next",
+ "History tabs stay open and read only vm-one, vm-two and agent-three with bounded GETs",
+ "delegated embedded buttons, native fragment links and checkbox clicks retain their own action without firing the row",
+ "shared disclosure touch and Enter/Space activate once; disabled/static rows do not act; restoring the row handler works",
+ "PulseDataGrid row activates once and its embedded button does not activate the row; no page errors or document horizontal overflow"
],
"interactions": [
- "desktop native keyboard disclosure and History tab; Chromium phone row touch; WebKit phone native keyboard disclosure (whole-row touch opening not established)",
- "focus native observation input, use Home/ArrowRight/End and compare dated values and same-time markers between utilization and network",
- "Chromium/WebKit phone-emulated touch taps on the at-least-44px native History input; inspection performs no extra API reads",
- "fail a real query refresh; retry with advancing bounds; hold and fulfil a 1-hour replacement read; refresh empty, choose seven days and a licence-locked range"
+ "phone-emulated locator.tap on ordinary PBS cells without focus, force, synthetic click dispatch or added event instrumentation",
+ "tap History, inspect real plotted paths, close each drawer from its same name cell; focus native disclosure and use Enter/Space",
+ "tap embedded button, fragment link and checkbox; tap and keyboard-activate native disclosure; withdraw and restore row action; activate data-grid row and its child button"
],
- "command": "pulse-worker-browser frontend-modern/browser-tests/history-window.cjs --engine=chromium --theme=light; --engine=chromium --theme=dark; both themes with --engine=chromium --phone; both themes with --engine=firefox; both themes with --engine=webkit --phone. Each is a separate bounded command from the assigned workspace root. Vite 6.4.3 / Playwright 1.56.1; integration-lock parity checked.",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/table-row-touch.cjs --engine=webkit; --engine=webkit --dark; --engine=chromium; --engine=firefox --dark --desktop. Four separate bounded executions from the assigned workspace root. Playwright 1.56.1 / integration-lock parity checked; Vite 6.4.3.",
"browser_versions": {
"chromium": "141.0.7390.37",
"firefox": "142.0.1",
"webkit": "26.0"
},
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-hour-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/retained-window-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/three-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/two-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-hour-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/retained-window-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/three-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/two-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-hour-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/retained-window-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/three-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/two-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-hour-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/retained-window-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/three-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/two-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-hour-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/retained-window-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/three-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/two-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-hour-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/retained-window-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/three-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/two-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-hour-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/retained-window-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/three-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/two-24h-dark.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-hour-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/progress.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/retained-window-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/three-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/two-24h-light.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-dark-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-light-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-dark-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-light-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-dark-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-light-final.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-dark-native.log",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-light-native.log"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/pbs-first-activation.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/pbs-third-history.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/shared-controls.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/pbs-first-activation.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/pbs-third-history.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/shared-controls.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/pbs-first-activation.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/pbs-third-history.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/shared-controls.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/pbs-first-activation.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/pbs-third-history.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/shared-controls.png"
],
- "notes": "All 40 final screenshots inspected. Eight engine/viewport/theme cases pass with no page errors or horizontal overflow and exact final runtime hashes. The ten-case exact-base source-fbz6bjig input fails all ten assertions; an inspected full PBS drawer baseline measures five minutes occupying 265/318 SVG plot units under 24 hours, versus about 1.10 units after repair. Initial navigation timeout, unknown browser-execution capability-unavailable exit, synthetic edge-point fixture mismatch and unsuccessful WebKit row/hidden-button touch attempts remain in internal receipts. WebKit final opening uses native keyboard disclosure, not a claimed touch repair; History touch inspection passes. Six Chromium/Firefox cases ran before the runner added the disclosure-state assertion and WebKit keyboard-opening branch; their runtime source hashes are identical to the final two WebKit cases. This establishes presentation, not real PBS/VirtualBox collection, all reported #1723 drawer/metric outcomes, screen-reader speech, installed Tailscale Serve/SSE update or signed upgrade/rollback, protected integration or release availability.",
+ "notes": "All 12 final screenshots inspected. This repairs reproduced WebKit-emulated row touch, not physical iPhone/Safari acceptance, installed PBS telemetry, every #1723 metric outcome, screen-reader speech or a Tailscale Serve update. Initial harness working-directory/API-path/locator/output-wrapping failures and the instrumented diagnostic are retained in the output receipts; none is a release failure or passing uninstrumented baseline.",
"prior_verifications": [
+ {
+ "version": 1,
+ "base_sha": "00f82e2afc3cd75035c997e9e245c6e0eeffffa9",
+ "verified_at": "2026-10-01T01:42:43Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx",
+ "frontend-modern/src/components/Workloads/guestDrawerModel.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "a49f3c6496e5515284a94c52b818f78f72d2e3530ae398794ea7432f251f48ec",
+ "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5ea99d3d995ddb2fa777273ed3f224b7fbb9e7b10a015bed52d61a8e10dc0b99"
+ },
+ "routes": [
+ "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer, shared History/query and CSS with synthetic VM/Agent APIs; not installed collection or complete #1723 acceptance)"
+ ],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "light/dark short sample coverage stays at its true position in the fulfilled 24-hour window rather than stretching across each plot",
+ "all configured utilization/network/disk/thermal panels share dated endpoints, including lone and empty panels",
+ "three separately mapped synthetic PBS drawers read vm-one, vm-two and agent-three, not unrelated host identities",
+ "same-time observations occupy the same horizontal coordinate across groups with different sample coverage",
+ "failed same-source refresh retains samples and window with a sanitised warning; successful refresh advances the window",
+ "held 1-hour range replacement clears former endpoints, paths and inspection controls; matching data then uses the new window",
+ "empty window has labelled bounds without a synthetic trend; seven-day bounds remain dated; locked fourteen-day range makes no read"
+ ],
+ "interactions": [
+ "desktop native keyboard disclosure and History tab; Chromium phone row touch; WebKit phone native keyboard disclosure (whole-row touch opening not established)",
+ "focus native observation input, use Home/ArrowRight/End and compare dated values and same-time markers between utilization and network",
+ "Chromium/WebKit phone-emulated touch taps on the at-least-44px native History input; inspection performs no extra API reads",
+ "fail a real query refresh; retry with advancing bounds; hold and fulfil a 1-hour replacement read; refresh empty, choose seven days and a licence-locked range"
+ ],
+ "command": "pulse-worker-browser frontend-modern/browser-tests/history-window.cjs --engine=chromium --theme=light; --engine=chromium --theme=dark; both themes with --engine=chromium --phone; both themes with --engine=firefox; both themes with --engine=webkit --phone. Each is a separate bounded command from the assigned workspace root. Vite 6.4.3 / Playwright 1.56.1; integration-lock parity checked.",
+ "browser_versions": {
+ "chromium": "141.0.7390.37",
+ "firefox": "142.0.1",
+ "webkit": "26.0"
+ },
+ "artifacts": [
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-hour-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/retained-window-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/three-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/two-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-hour-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/retained-window-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/three-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/two-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-hour-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/retained-window-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/three-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/two-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-hour-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/retained-window-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/three-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/two-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-hour-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/retained-window-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/three-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/two-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-hour-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/retained-window-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/three-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/two-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-hour-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/retained-window-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/three-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/two-24h-dark.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-hour-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/progress.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/retained-window-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/three-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/two-24h-light.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-dark-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-light-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-dark-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-light-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-dark-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-light-final.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-dark-native.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-light-native.log"
+ ],
+ "notes": "All 40 final screenshots inspected. Eight engine/viewport/theme cases pass with no page errors or horizontal overflow and exact final runtime hashes. The ten-case exact-base source-fbz6bjig input fails all ten assertions; an inspected full PBS drawer baseline measures five minutes occupying 265/318 SVG plot units under 24 hours, versus about 1.10 units after repair. Initial navigation timeout, unknown browser-execution capability-unavailable exit, synthetic edge-point fixture mismatch and unsuccessful WebKit row/hidden-button touch attempts remain in internal receipts. WebKit final opening uses native keyboard disclosure, not a claimed touch repair; History touch inspection passes. Six Chromium/Firefox cases ran before the runner added the disclosure-state assertion and WebKit keyboard-opening branch; their runtime source hashes are identical to the final two WebKit cases. This establishes presentation, not real PBS/VirtualBox collection, all reported #1723 drawer/metric outcomes, screen-reader speech, installed Tailscale Serve/SSE update or signed upgrade/rollback, protected integration or release availability."
+ },
{
"version": 1,
"base_sha": "0dc858cb366a394b159d2c911c5d85b3020e7881",
"verified_at": "2026-10-01T00:37:51Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "e1658b5843ab22e0ea83d1bf27e8c1565a16d9f528eb3832524a67318d281d6a"
},
@@ -192,7 +257,9 @@
"base_sha": "0d3e5a801ab8fd1c1d224ef561cbf99e169c589f",
"verified_at": "2026-09-30T23:28:26Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "327e75cabb99edffc712302c00645f06d92dc44dbebdd00aef08074e7fa53eb2"
},
@@ -332,7 +399,9 @@
"base_sha": "21a35494aecb335755de773dd099e1364e65b3e0",
"verified_at": "2026-09-30T20:12:09Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "b2b31441b19c2e50283ceedf319cd09a1f94791750cd424c052717a4957fdc67"
},
@@ -387,7 +456,9 @@
"base_sha": "6ade01cbd4d02cd25b691f9ff5919206cceae764",
"verified_at": "2026-09-30T01:40:13Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "ed7207456ebbc990ada34b0c739e29565d90cfe1998415a781d87f3067d69c36"
},
@@ -436,7 +507,9 @@
"base_sha": "c3e8b6dd6dff36b5396ba2eb147d4e58d23001a6",
"verified_at": "2026-09-29T23:04:22Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "7172690587f8e642ee30492481abbe76ee79d277aad290c3b50310dd1c0b282b"
},
@@ -532,7 +605,9 @@
"base_sha": "fbf17b271abe70ad3e12042506b52f85b6cbc484",
"verified_at": "2026-09-29T19:50:45Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/pages/Actions.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/pages/Actions.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/pages/Actions.tsx": "30e862ecc5125f07cf93f0c1e0b5f58efa023f5fdb56fe4b01721bf9a453e7ce"
},
@@ -573,7 +648,9 @@
"base_sha": "c2f7aa8b471d6cbb858e8ec71b4e664fded6549c",
"verified_at": "2026-09-29T19:09:52Z",
"result": "passed",
- "changed_paths": ["frontend-modern/src/features/actions/ActionReviewDialog.tsx"],
+ "changed_paths": [
+ "frontend-modern/src/features/actions/ActionReviewDialog.tsx"
+ ],
"content_sha256": {
"frontend-modern/src/features/actions/ActionReviewDialog.tsx": "9a4af522484b42e37b118e8bfde7c1673122037a54b8bfed249818ba644e6b81"
},
diff --git a/frontend-modern/src/components/shared/Table.tsx b/frontend-modern/src/components/shared/Table.tsx
index 6d2fdbb506..472263d205 100644
--- a/frontend-modern/src/components/shared/Table.tsx
+++ b/frontend-modern/src/components/shared/Table.tsx
@@ -63,11 +63,17 @@ export function TableBody(props: TableBodyProps) {
export type TableRowProps = JSX.HTMLAttributes;
+// WebKit touch needs a native click target on otherwise-static table rows.
+// Keep actions document-delegated: nested controls must run first and retain
+// their existing stopPropagation behaviour instead of opening the row too.
+const nativeRowClickTarget = () => undefined;
+
export function TableRow(props: TableRowProps) {
const [local, rest] = splitProps(props, ['class', 'children']);
return (
{local.children}
diff --git a/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx b/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx
index 4691fbf0fa..23ac2ff5e9 100644
--- a/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx
@@ -103,6 +103,23 @@ describe('PulseDataGrid', () => {
expect(onRowClick).toHaveBeenCalledTimes(1);
});
+ it('inherits the native touch click target from the shared TableRow', () => {
+ const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener');
+ try {
+ render(() => (
+
+ data={[{ id: '1', name: 'Touch tower' }]}
+ columns={[{ key: 'name', label: 'Name' }]}
+ keyExtractor={(row) => row.id}
+ onRowClick={vi.fn()}
+ />
+ ));
+ expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(true);
+ } finally {
+ nativeListener.mockRestore();
+ }
+ });
+
it('does not trigger the row handler when an interactive child is clicked', () => {
const onRowClick = vi.fn();
const onRemove = vi.fn();
diff --git a/frontend-modern/src/components/shared/__tests__/Table.test.tsx b/frontend-modern/src/components/shared/__tests__/Table.test.tsx
index 484cecc996..c81ce1dde0 100644
--- a/frontend-modern/src/components/shared/__tests__/Table.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/Table.test.tsx
@@ -1,5 +1,6 @@
-import { render, screen } from '@solidjs/testing-library';
-import { describe, expect, it } from 'vitest';
+import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import tableSource from '@/components/shared/Table.tsx?raw';
import {
@@ -11,6 +12,128 @@ import {
TableRow,
} from '@/components/shared/Table';
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+describe('TableRow touch activation', () => {
+ it('marks clickable rows as native click targets without changing row semantics', () => {
+ const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener');
+ const onClick = vi.fn();
+ render(() => (
+
+ ));
+ expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(true);
+ const row = screen.getByRole('row');
+ expect(row).not.toHaveAttribute('tabindex');
+ expect(row).not.toHaveAttribute('aria-expanded');
+ fireEvent.click(screen.getByText('Tap resource'));
+ expect(onClick).toHaveBeenCalledTimes(1);
+ fireEvent.keyDown(row, { key: 'Enter' });
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not make static rows native click targets', () => {
+ const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener');
+ render(() => (
+
+
+
+ Static resource
+
+
+
+ ));
+ expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(false);
+ });
+
+ it('lets delegated child actions stop the row before its action runs', () => {
+ const onClick = vi.fn();
+ const onChild = vi.fn();
+ render(() => (
+
+
+
+
+
+
+
+
+
+ ));
+ fireEvent.click(screen.getByRole('button', { name: 'Child action' }));
+ expect(onChild).toHaveBeenCalledTimes(1);
+ expect(onClick).not.toHaveBeenCalled();
+ });
+
+ it('removes the native target when the row action is withdrawn', () => {
+ const removeListener = vi.spyOn(HTMLTableRowElement.prototype, 'removeEventListener');
+ const onClick = vi.fn();
+ const [enabled, setEnabled] = createSignal(true);
+ render(() => (
+
+
+
+ Changing resource
+
+
+
+ ));
+ fireEvent.click(screen.getByText('Changing resource'));
+ setEnabled(false);
+ expect(removeListener.mock.calls.some(([type]) => type === 'click')).toBe(true);
+ fireEvent.click(screen.getByText('Changing resource'));
+ expect(onClick).toHaveBeenCalledTimes(1);
+ setEnabled(true);
+ fireEvent.click(screen.getByText('Changing resource'));
+ expect(onClick).toHaveBeenCalledTimes(2);
+ });
+
+ it('preserves bound delegated handlers and caller-owned native handlers', () => {
+ const onClick = vi.fn();
+ const onNative = vi.fn();
+ render(() => (
+
+
+ {
+ event.stopPropagation();
+ onNative();
+ }}
+ >
+ Native action
+
+
+ Bound action
+
+
+
+ ));
+ fireEvent.click(screen.getByText('Native action'));
+ expect(onNative).toHaveBeenCalledTimes(1);
+ expect(onClick).not.toHaveBeenCalled();
+ fireEvent.click(screen.getByText('Bound action'));
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClick.mock.calls[0][0]).toBe('second-identity');
+ });
+});
+
describe('TableBody', () => {
it('keeps the shared table wrapper CSP-safe', () => {
expect(tableSource).toContain('touch-scroll');