diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 560a03e88..2eec02ad8 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -336,10 +336,23 @@ These proofs establish presentation/recovery, not installed collection or delive Shared workload, node, Docker-host, and resource-drawer history presentation keeps current readings separate from stored samples. A current metric may -populate the legend while history is still being collected, but it must never -be expanded into synthetic timestamps or chart geometry. An empty stored -series renders the shared collecting-history state; zero remains a valid +populate the legend only with the explicit `current` label, never as a stored +reading, timestamp or chart geometry. A successful empty read says `No stored +history in this range`; it does not imply that collection is running or that +changing provider configuration will produce a sample. Zero remains a valid reported reading, while an absent metric remains unavailable. +A lone stored series renders one point at its actual timestamp and value, +including measured zero, never an invented line. A group with one observation +time exposes that date visibly with `Single observation. No trend yet.` and +retains the existing accessible observation description. Multiple series with +separate single observations remain separate points, inspectable at their +actual times through the existing native control. Failed same-source refreshes +retain these points; source/range replacement clears them along with paths and +captions. `GuestDrawerHistory.sparse.test.tsx` checks geometry, units, provenance, +invalid data, refresh and replacement. `browser-tests/history-window.cjs` also +checks sparse/empty/current states through the production PBS drawer and direct +shared renderer. This is presentation proof, not provider collection or #1723 +installed acceptance. Shared history-chart gridlines must carry numeric labels derived from the plotted scale rather than semantic `Avg` / `Max` placeholders. Byte and byte-rate axes include their human-readable unit at each gridline, and the @@ -708,7 +721,7 @@ the synthetic estate continues updating, plus the alert timeline and note form. The Backups surface passes its complete deduplicated route inventory to the PBS table. Only PBS servers render as rows; other resources supply correlation inputs. Preserve PBS drawer identity and use the correlated canonical history target. Missing disk utilisation does not gate CPU/memory history. -One agent can be surfaced twice for a single PBS host: folded into its PVE guest and as a standalone `source=pbs` host row. Those two rows are one machine, not an ambiguous pair. Correlation must collapse candidates that share an agent identity and prefer the guest representation, whose canonical metrics target carries the persisted host history; the PBS service target has no host series and renders the collecting-history state. Two candidates with distinct agent identities remain ambiguous, and a candidate with no agent identity must not be treated as proof of sameness. +One agent can be surfaced twice for a single PBS host: folded into its PVE guest and as a standalone `source=pbs` host row. Those two rows are one machine, not an ambiguous pair. Correlation must collapse candidates that share an agent identity and prefer the guest representation, whose canonical metrics target carries the persisted host history; the PBS service target has no stored host series and renders the no-history state. Two candidates with distinct agent identities remain ambiguous, and a candidate with no agent identity must not be treated as proof of sameness. A live snapshot can briefly omit the correlated host row while the PBS server row remains, for example while a realtime refresh replaces the merged estate. The correlation must retain the last resolved host per PBS server across that omission instead of falling back to the PBS service target, so the drawer's Discovery and Metrics Target rows and its History series do not flicker. Reuse the remembered host only while it is still fresh relative to the server, and drop it once stale so a removed or replaced host is not advertised indefinitely; a host row that is present but ambiguous still declines. diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index ed0ab8c5e..50dd1df94 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -876,11 +876,17 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. the migration boundary. 11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx` Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`. - A current metric may remain visible in a chart legend while history is - accumulating, but it must never be expanded into synthetic timestamps or a - flat line. Fewer than two stored points remains `Collecting history` and - only metrics-store samples may contribute chart geometry, bounds, or hover - inspection. Current-value legends must preserve the same availability + A current metric may remain visible in a chart legend only when labelled + `current`; it must never be expanded into synthetic timestamps or a flat + line. One stored sample is a visible point at its actual time, with a dated + single-observation caption rather than a collecting claim or invented trend. + Successful empty reads say `No stored history in this range`. Only + metrics-store samples may contribute chart geometry, bounds, or hover + inspection, including valid measured zero. This presentation adds no fetch, + point expansion, polling or retention budget. Verify sparse and mixed-series + geometry and failed-refresh/source-change behaviour in + `GuestDrawerHistory.sparse.test.tsx`, with rendered phone/desktop coverage in + `browser-tests/history-window.cjs`. Current-value legends preserve the same availability semantics as workload rows: the poller's negative unknown-disk sentinel is unavailable and renders `-`, never a fabricated negative percentage. History cards must let the plot area stretch to the card height instead of diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index feeb999fb..c4689a77c 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -5223,6 +5223,7 @@ "frontend-modern/src/components/SetupWizard/SetupWizard.tsx", "frontend-modern/src/components/Toast/Toast.tsx", "frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx", + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx", "frontend-modern/src/components/Workloads/nodeDrawerModel.ts", "frontend-modern/src/features/docker/dockerHostDrawerModel.ts", "frontend-modern/src/features/platformPage/platformEstateOverviewModel.ts", @@ -5929,6 +5930,24 @@ "frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts", "frontend-modern/src/pages/__tests__/Docs.test.ts" ] + }, + { + "id": "drawer-metric-history", + "label": "shared drawer stored/current history proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.inspection.test.tsx", + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.pointer.test.tsx", + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.refresh.test.tsx", + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.source-isolation.test.tsx", + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.sparse.test.tsx", + "frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.window.test.tsx" + ] } ], "match_files": null diff --git a/frontend-modern/browser-tests/history-window.cjs b/frontend-modern/browser-tests/history-window.cjs index ea0a9bce7..c04342a8e 100644 --- a/frontend-modern/browser-tests/history-window.cjs +++ b/frontend-modern/browser-tests/history-window.cjs @@ -138,7 +138,15 @@ const { chromium, firefox, webkit } = require('playwright'); } if (state === 'failure') return route.fulfill({ status: 503, json: { error: 'Private fixture detail' } }); - return route.fulfill({ json: response(type, id, range, state === 'empty') }); + const result = response(type, id, range, state === 'empty'); + if (state === 'sparse') + result.metrics = { + cpu: [point(5, 0)], + netin: [point(5, 0)], + diskwrite: [point(5, 0)], + temperature: [point(5, 50)], + }; + return route.fulfill({ json: result }); } if (url.pathname === '/api/license/runtime-capabilities') return route.fulfill({ @@ -336,14 +344,54 @@ const { chromium, firefox, webkit } = require('playwright'); ); await detail.screenshot({ path: path.join(artifacts, `one-hour-${theme}.png`) }); progress('replacement-window-verified', { theme }); - state = 'empty'; + state = 'sparse'; await detail.getByRole('button', { name: 'Refresh history' }).click(); await page.waitForFunction( () => document.querySelectorAll('[data-testid="guest-history-plot"] path').length === 0, ); + await detail.screenshot({ path: path.join(artifacts, `sparse-${theme}.png`) }); + const sparseObservation = { + theme, + pointCount: await detail.locator('[data-history-observation]').count(), + collectingClaims: await detail.getByText('Collecting history', { exact: true }).count(), + descriptions: await detail + .getByTestId('guest-history-plot') + .evaluateAll((plots) => + plots.map( + (plot) => document.getElementById(plot.getAttribute('aria-describedby'))?.textContent, + ), + ), + }; + progress('sparse-observation', sparseObservation); + assert.equal(sparseObservation.pointCount, 4, 'each lone series must be visible'); + assert.equal(sparseObservation.collectingClaims, 0); + assert.equal(await detail.getByRole('slider').count(), 0); + assert.equal(await detail.getByText('Single observation. No trend yet.').count(), 4); + for (const dot of await detail.locator('[data-history-observation]').all()) { + assert.ok(Math.abs(Number(await dot.getAttribute('cx')) - expectedX(5, '1h')) < 0.01); + assert.ok(Number.isFinite(Number(await dot.getAttribute('cy')))); + } + await verifyWindow(detail, '1h'); + state = 'failure'; + await detail.getByRole('button', { name: 'Refresh history' }).click(); + await detail + .getByText('History refresh failed. Showing previously loaded history.') + .waitFor(); + assert.equal(await detail.locator('[data-history-observation]').count(), 4); + assert.equal(await detail.getByText('Single observation. No trend yet.').count(), 4); + state = 'empty'; + await detail.getByRole('button', { name: 'Retry history' }).click(); + await page.waitForFunction( + () => document.querySelectorAll('[data-history-observation]').length === 0, + ); await verifyWindow(detail, '1h'); assert.equal(await detail.getByRole('slider').count(), 0); - assert.equal(await detail.getByText('Collecting history', { exact: true }).count(), 4); + assert.equal( + await detail.getByText('No stored history in this range', { exact: true }).count(), + 4, + ); + assert.equal(await detail.getByText('Single observation. No trend yet.').count(), 0); + await detail.screenshot({ path: path.join(artifacts, `empty-${theme}.png`) }); state = 'success'; await detail.getByRole('combobox', { name: 'History range' }).selectOption('7d'); await utilization.getByTestId('guest-history-plot').locator('path').first().waitFor(); @@ -354,6 +402,23 @@ const { chromium, firefox, webkit } = require('playwright'); assert.equal(requests.length, readsBeforeLock); assert.equal(await detail.getByTestId('guest-history-time-window').count(), 0); assert.equal(errors.length, 0, errors.join('\n')); + // The direct production renderer deliberately supplies current readings. + // A live fallback must be labelled, never plotted or attributed to a stored time. + state = 'empty'; + await page.goto('http://127.0.0.1:5225/browser-tests/pbs-history-refresh.html'); + await page.evaluate( + (dark) => document.documentElement.classList.toggle('dark', dark), + theme === 'dark', + ); + detail = page.getByTestId('history-refresh-fixture'); + await detail.getByText('No stored history in this range', { exact: true }).first().waitFor(); + const liveCPU = detail.locator( + '[data-history-group="utilization"] [data-history-current="cpu"]', + ); + assert.match((await liveCPU.innerText()).replace(/\s+/g, ' '), /CPU\s*42\.0%\s*current/); + assert.equal(await detail.locator('[data-history-observation]').count(), 0); + assert.equal(await detail.getByTestId('guest-history-plot').locator('path').count(), 0); + await detail.screenshot({ path: path.join(artifacts, `current-empty-${theme}.png`) }); observations.push({ theme, requests, errors, utilizationX, networkX }); progress('completed-theme', { theme }); await page.close(); diff --git a/frontend-modern/browser-tests/pbs-history-refresh.cjs b/frontend-modern/browser-tests/pbs-history-refresh.cjs index 2eb3133e9..9a61945e5 100644 --- a/frontend-modern/browser-tests/pbs-history-refresh.cjs +++ b/frontend-modern/browser-tests/pbs-history-refresh.cjs @@ -209,7 +209,10 @@ const { chromium } = require('playwright'); await page.getByRole('button', { name: 'Switch to service target', exact: true }).click(); await status.getByText('Failed to load history data', { exact: true }).waitFor(); assert.equal(await plots.count(), 0); - assert.equal(await detail.getByText('Collecting history', { exact: true }).count(), 0); + assert.equal( + await detail.getByText('No stored history in this range', { exact: true }).count(), + 0, + ); assert.equal(await detail.getByText(/previously loaded/).count(), 0); await checkLayout(); await screenshot('replacement-failed'); diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 51ed3966f..8bfdbcd2d 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,17 +1,17 @@ { "version": 1, - "base_sha": "920aa2f27c6cb717cca7eb8e2cd3f1379d0fdf73", - "verified_at": "2026-10-01T03:23:21Z", + "base_sha": "36418af66beac94204276f7dc73de2c41e297fb1", + "verified_at": "2026-10-01T05:22:37Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/shared/Table.tsx" + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" ], "content_sha256": { - "frontend-modern/src/components/shared/Table.tsx": "4009a751a3fa435cb4067a98989d25cac4e9e6d08acd7b91701fc98fbfd5aaf1" + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "65538187de57957691eb00af08a9f86b916a6300f2d12065a1985fb2170ef7bf" }, "routes": [ - "/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)" + "/browser-tests/pbs-identity-boundary.html (production PBS table and full ResourceDetailDrawer/History; synthetic bounded API/Agent/PVE resources)", + "/browser-tests/pbs-history-refresh.html (production shared History renderer with deliberately distinct current readings)" ], "viewports": [ { @@ -24,43 +24,131 @@ } ], "states": [ - "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" + "All three synthetic PBS drawers retain vm-one, vm-two and agent-three history identity and one dated window", + "A lone stored CPU, network, disk-write and thermal series each displays one point at its actual time, with valid measured zeros and no manufactured path", + "Single-time panels visibly date their observation and explain that it is not a trend; empty successful reads report no stored history in the selected range", + "Live CPU/memory legend fallbacks carry an explicit current label and create no stored geometry", + "Failed same-source refresh retains lone points and the warning; uncached range replacement clears old geometry and captions while loading; locked ranges make no reads", + "Light/dark phone and desktop rendering: no page errors or document horizontal overflow" ], "interactions": [ - "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" + "Native disclosure and History tabs for each of three mapped drawers; keyboard/touch stored-time inspection remains dated and makes no extra reads", + "Refresh, failed refresh, retry, sparse response, successful empty response, and 24h/1h/7d/locked-14d range changes", + "Current-only direct renderer read confirms explicitly labelled live readings and empty plots" ], - "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.", + "command": "Four executions from assigned workspace root: pulse-worker-browser frontend-modern/browser-tests/history-window.cjs --engine=webkit --phone --theme=light; --engine=webkit --phone --theme=dark; --engine=firefox --theme=dark; --engine=chromium --phone --theme=light. Playwright 1.56.1 matches integration lock; Vite 6.4.3.", "browser_versions": { - "chromium": "141.0.7390.37", + "webkit": "26.0", "firefox": "142.0.1", - "webkit": "26.0" + "chromium": "141.0.7390.37" }, "artifacts": [ - "/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" + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/sparse-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/current-empty-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/empty-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-light/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/current-empty-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/sparse-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/empty-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-webkit-390-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/current-empty-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/sparse-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/empty-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-firefox-1365-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/sparse-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/current-empty-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/empty-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-_7g3zqr_/browser-qualified/history-window-chromium-390-light/progress.json" ], - "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.", + "notes": "All 32 final screenshots inspected after the copy-style correction to Single observation. No trend yet. All four fresh passes match the runtime SHA256 above. Baseline WebKit phone returned exit 1: four valid lone series had zero points and four Collecting history claims. Earlier HMR/source-hash diagnostics and a batch wrapper exit 143 are retained outside this final proof; the final single Chromium execution exits 0. Offline renderer fixtures do not establish installed PBS/VirtualBox/provider acceptance, physical devices, screen-reader speech or Tailscale Serve update proof. No API, polling, cache or license boundary changes.", "prior_verifications": [ + { + "version": 1, + "base_sha": "920aa2f27c6cb717cca7eb8e2cd3f1379d0fdf73", + "verified_at": "2026-10-01T03:23:21Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/shared/Table.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/shared/Table.tsx": "4009a751a3fa435cb4067a98989d25cac4e9e6d08acd7b91701fc98fbfd5aaf1" + }, + "routes": [ + "/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": 390, + "height": 844 + }, + { + "width": 1365, + "height": 900 + } + ], + "states": [ + "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": [ + "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/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-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 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." + }, { "version": 1, "base_sha": "00f82e2afc3cd75035c997e9e245c6e0eeffffa9", diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index dd722dc9d..a7c1fd727 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -161,7 +161,7 @@ const baseResource = (overrides: Partial): Resource => ({ describe('ResourceDetailDrawer change history section', () => { it('keeps current readings separate from stored history samples', () => { expect(guestDrawerHistorySource).toContain('currentMetrics'); - expect(guestDrawerHistorySource).toContain('Collecting history'); + expect(guestDrawerHistorySource).toContain('No stored history in this range'); expect(guestDrawerHistorySource).not.toContain('buildFallbackHistoryPoints'); }); diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index 8b44aefcb..adb3254cc 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -556,8 +556,8 @@ describe('GuestDrawer', () => { await waitFor(() => expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalled()); const utilizationChart = screen.getAllByTestId('guest-history-group-chart')[0]; - expect(utilizationChart).toHaveTextContent('CPU25.0%'); - expect(utilizationChart).toHaveTextContent('Collecting history'); + expect(utilizationChart).toHaveTextContent('CPU25.0%current'); + expect(utilizationChart).toHaveTextContent('No stored history in this range'); expect(utilizationChart.querySelector('path')).toBeNull(); }); diff --git a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx index 6cf02dac9..427b7ec2f 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx @@ -274,9 +274,13 @@ const GuestDrawerHistoryGroupChart: Component return `${count} stored observation ${count === 1 ? 'time' : 'times'}. ${inspectionValueText()}`; }); const drawableSeries = createMemo(() => series().filter((item) => item.points.length >= 2)); + const singlePointSeries = createMemo(() => series().filter((item) => item.points.length === 1)); + const singleObservation = createMemo(() => + observationTimes().length === 1 ? { timestamp: observationTimes()[0] } : null, + ); const scale = createMemo(() => getGuestDrawerHistoryScale(series(), props.group.unit)); const bounds = () => props.timeBounds; - const hasDrawableData = createMemo(() => drawableSeries().length > 0 && bounds() !== null); + const hasStoredData = createMemo(() => observationTimes().length > 0 && bounds() !== null); const hoveredSeries = createMemo(() => { const timestamp = activeTimestamp(); const rangeBounds = bounds(); @@ -309,6 +313,11 @@ const GuestDrawerHistoryGroupChart: Component const currentValue = props.currentMetrics?.[item.metric]; return { ...item, + isCurrent: + activeTimestamp() === null && + item.points.length === 0 && + typeof currentValue === 'number' && + Number.isFinite(currentValue), valueLabel: hovered ? getGuestDrawerHistoryValueLabel([hovered.point], item.unit) : activeTimestamp() !== null @@ -369,12 +378,18 @@ const GuestDrawerHistoryGroupChart: Component
{(item) => ( - + {item.label} {item.valueLabel} + + current + )} @@ -421,26 +436,48 @@ const GuestDrawerHistoryGroupChart: Component {(rangeBounds) => ( - - {(item) => ( - - )} - + <> + + {(item) => ( + + )} + + + {(item) => ( + + )} 0}> @@ -471,9 +508,9 @@ const GuestDrawerHistoryGroupChart: Component - +
- Collecting history + No stored history in this range
@@ -515,6 +552,19 @@ const GuestDrawerHistoryGroupChart: Component

{chartDescription()}

+ + {(observation) => ( +

+ Single observation. No trend yet. + +

+ )} +
1}>