From 67104c830d39d2b798a45122d7df1dcad2844067 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:21:42 +0100 Subject: [PATCH] Show guest-read safety explanations without hover The existing Workloads drawer puts the meaning of reported availability and cached metadata only in a title. Show that fixed explanation directly beneath the read state so phone and keyboard users can see that it does not establish thaw, writes or a current connection. Deferred states retain their existing prominent warning rather than repeating it in the row. Add visible-text and same-guest transition controls, with source-bound desktop and phone browser proof. Contract-Neutral: Rendering existing guest-read explanations visibly; no state selection, collector, metric, permission or guest-command contract change. Change-source: pulse-maintainer --- .../119fba64dc909865.json | 39 +++++++++++++++++++ .../Workloads/GuestDrawerOverview.tsx | 30 +++++++++++--- .../GuestDrawerOverview.agent-state.test.tsx | 34 ++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 frontend-modern/browser-verification/119fba64dc909865.json diff --git a/frontend-modern/browser-verification/119fba64dc909865.json b/frontend-modern/browser-verification/119fba64dc909865.json new file mode 100644 index 0000000000..454807796b --- /dev/null +++ b/frontend-modern/browser-verification/119fba64dc909865.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "base_sha": "020a7be6bf2a2923c291395a8409ed79ddcde336", + "verified_at": "2026-10-12T13:19:44.508Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "af9cd697e786db4ead5a3ea02b923c4d95bbfbd32c22457db11c9ee0a1f74371" + }, + "routes": [ + "/proxmox (production GuestDrawer in a local synthetic read-state fixture)" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Available, expected-unreachable, not-running, disabled, unrecognised and cached-version-only: original fixed explanations are visible without hover; unknown raw values are not rendered.", + "Backup lock and timed-out read retain the original prominent safety warning/link without duplicate row text; available state returns on the same guest after clearance. Clearance is not native thaw/recovery.", + "LXC and vSphere remain without a QEMU read-state row.", + "22 source-bound desktop Chromium 141.0.7390.37 / phone WebKit 26.0 checks on Playwright 1.56.1; four final captures inspected with native view_image. Source imports: 164 verified loaded files.", + "Explicit empty GET /api/resources and GET /api/alerts/active fixtures only; no API writes or external service. Loopback WebSocket proxy ECONNREFUSED remains in raw browser log; no streaming/native acceptance.", + "Prior clients 1/1/1 retained: navigation readiness failures and the zero-read fixture oracle failure are not passes. Final client 0, browsers/contexts/server closed." + ], + "interactions": [ + "Changed provider-owned state for the same canonical guest without remount, checking visible state/detail, geometry and precautions.", + "Focused the production collapse button and pressed Enter, then reopened the fixture drawer and read the current explanation.", + "Changed fixture platform to LXC/vSphere and confirmed no misleading QEMU explanation." + ], + "evidence": "Lane outcome RECEIPTS.md links complete results/scripts/captures/native observations and retains adverse attempts; not a real guest, current-source backend or released proof." +} diff --git a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx index 95469bb425..319748c473 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx @@ -79,6 +79,30 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { ageClass: props.backupPresentation?.ageClass, }); const guestReadPresentation = () => getGuestDrawerGuestReadPresentation(props.guest); + const guestReadRow = () => { + const read = guestReadPresentation(); + if (!read) return null; + return makeDetailRow('Guest-agent reads', read.label, { + title: read.detail, + tone: read.tone, + wrap: true, + // A title alone is not available to touch users. Deferred reads already + // have a visible safety notice above the drawer; do not repeat it here. + layout: read.precaution ? undefined : 'stacked', + valueContent: ( + <> + + {read.label} + + + + {read.detail} + + + + ), + }); + }; const memoryReading = () => getGuestDrawerMemoryReading(props.guest); // A linked Pulse agent past its reporting lease leaves its last report on // the guest, so its RAID, network and filesystem rows read as last known. @@ -104,11 +128,7 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { : undefined, wrap: true, }), - makeDetailRow('Guest-agent reads', guestReadPresentation()?.label, { - title: guestReadPresentation()?.detail, - tone: guestReadPresentation()?.tone, - wrap: true, - }), + guestReadRow(), makeDetailRow('Memory reading', memoryReading()?.summary, { wrap: true, }), diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx index 669b746599..21011baffc 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx @@ -63,6 +63,40 @@ const readRow = () => screen.getByText('Guest-agent reads').closest('tr')!; afterEach(cleanup); describe('guest agent coverage requires current state, not cached metadata', () => { + it.each([ + 'available', + 'expected-unreachable', + 'not-running', + 'disabled', + 'future-state-private-detail', + '', + ])('makes the %s explanation visible without hovering', (state) => { + const value = guest({ guestAgentStatus: state, agentVersion: '9.2' }); + render(() => ); + const presentation = getGuestDrawerGuestReadPresentation(value)!; + expect(within(readRow()).getByText(presentation.detail)).toBeVisible(); + expect(readRow()).not.toHaveTextContent('future-state-private-detail'); + expect(readRow().querySelector('[tabindex]')).toBeNull(); + }); + + it('updates the visible explanation without repeating the deferred safety notice', () => { + const [value, setValue] = createSignal(guest({ guestAgentStatus: 'available' })); + render(() => ); + const availableDetail = getGuestDrawerGuestReadPresentation(value())!.detail; + expect(within(readRow()).getByText(availableDetail)).toBeVisible(); + setValue({ ...value(), lock: 'backup', diskStatusReason: 'prev-vm-locked' }); + expect(within(readRow()).getByText('Deferred')).toBeVisible(); + expect(within(readRow()).queryByText(availableDetail)).not.toBeInTheDocument(); + expect( + within(readRow()).queryByText(getGuestDrawerGuestReadPresentation(value())!.detail), + ).not.toBeInTheDocument(); + setValue({ ...value(), lock: '', diskStatusReason: '', guestAgentStatus: 'not-running' }); + expect( + within(readRow()).getByText(getGuestDrawerGuestReadPresentation(value())!.detail), + ).toBeVisible(); + expect(within(readRow()).queryByText(availableDetail)).not.toBeInTheDocument(); + }); + it.each([ ['deferred', 'Deferred'], ['expected-unreachable', 'Unreachable'],