diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index c0943f501d..3c89b76df0 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -15,6 +15,25 @@
## Purpose
+### Pulse system alerts retain their service scope in the existing UI
+
+The `pulse-system-` identity prefix or explicit `systemAlert: true` metadata
+marks a Pulse service condition, not a monitored resource. Overview retains
+the warning, acknowledgement, snooze and occurrence timeline but offers no
+resource link or monitoring-policy action. History preserves this scope for
+active and retained conditions before any resource-name fallback: a machine
+named Pulse is not evidence that it owns the service warning. Ordinary resource
+alerts, including those called Pulse, retain their existing target affordances.
+
+The Assistant explanation keeps the exact alert identity, message and
+approval-required boundary, without invented metric values, a guest target or
+resource Patrol. Reconstructed History explanations preserve the system marker;
+new system types need no frontend allowlist. Connected overview/history/handoff
+and investigation-button controls verify both authoritative markers, legacy
+metadata, name collisions and ordinary resource controls. Browser acceptance
+checks the actual components on desktop and narrow touch views. This changes
+presentation only, not alert evaluation, delivery, API or operator permissions.
+
### Backup-age evaluation failures use the existing system-alert lifecycle
`pulse-system-backup-evaluation` is a warning about Pulse's inability to read
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index cba4da2477..aa67fa3c82 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -20,6 +20,16 @@
## Purpose
+### Optional guest-memory observation reaches existing frontend consumers
+
+Frontend Memory and ResourceMetric model the existing optional server-owned
+`observation` fields (state, source, original observedAt). The shared wire
+reader selects only those fields and never invents an annotation for a legacy
+metric. API/canonical workload mapping preserves the selected annotation, not
+that of a conflicting platform facet. Unknown states/times stay non-current in
+the Proxmox guest drawer. Numeric selection, permissions and existing endpoint
+shape remain unchanged; hook/adapter/History controls verify this consumer.
+
### Platform connection alerts share the constructed runtime owners
The default monitor's connection-alert snapshot callback is installed only after
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index c6951e64ab..bd2fbe8a17 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -20,6 +20,42 @@
## Purpose
+### Guest drawers consume selected memory provenance
+
+Existing guest drawers consume optional server-owned `memory.observation` and
+selected canonical/API metric annotations without renewing their original
+observation time from resource LastSeen, disk-read deferral or another platform
+facet. `useWorkloads`, `useUnifiedResources` and the legacy resource adapter
+preserve the selected annotation. A selected unannotated metric cannot borrow a
+conflicting raw platform annotation. Raw-memory fallback retains its own
+annotation only when its numeric reading is actually selected.
+
+Proxmox VM/LXC Overview states current, last known, unavailable or freshness
+unknown with the original known UTC time and an allowlisted source label.
+Missing, unrecognised or unusable provenance is not current. Unavailable usage
+keeps known capacity but never invents zero used/free/swap measurements. History
+keeps retained/unknown numeric context separate from current legends and stored
+points, dated inspection and denied source access. Same-guest source changes
+update without resetting the drawer or refetching History. Independent valid
+PVE/Pulse Agent readings remain current even while QGA disk reads are deferred;
+unannotated unrelated platforms keep existing presentation.
+
+Connected original-source/normalizer/adapter and mounted History controls plus
+`browser-tests/guest-memory-provenance.cjs` exercise these boundaries. They do
+not establish native thaw, covered-filesystem writes, liveness, reporter relief,
+publication or an installed containing release. No route, collection, numeric
+selection, retention, alert policy or diagnostic permission is added.
+
+Pulse system-alert presentation uses the existing reserved ID/metadata markers,
+not the displayed name or a platform fallback. Overview and both History layouts
+keep service warnings, occurrence timelines and acknowledgement usable without
+a false resource link/policy action. History retains the system marker when
+reconstructing Assistant context; the explanation has no fabricated metric,
+guest target or targeted Patrol. Ordinary monitored resources named Pulse keep
+their original affordances. The shared scope helper, mounted alert controls and
+desktop/narrow-touch browser fixture cover this boundary. No navigation entry,
+page, API, alert policy or operator permission is introduced.
+
### Guest-agent coverage is not inferred from metadata
The existing workload drawer distinguishes assigned Pulse Agent action ownership,
@@ -39,8 +75,8 @@ requires independent thaw and writes to covered filesystems. Running power
state, an OK backup or reported agent availability establishes none of those.
Fresh same-guest evidence withdraws the precaution without a new diagnostic or
collector request. Existing filesystem provenance, metric values, History,
-Discovery/action permissions and monitoring policy are unchanged. No disk-read
-reason is used to invent memory provenance that the workload API does not expose.
+Discovery/action permissions and monitoring policy are unchanged. Memory qualification uses its own authoritative observation; disk-read state
+cannot qualify a different metric or renew its observation time.
Verification: `GuestDrawerOverview.agent-state.test.tsx` covers each current state
with/without cached version, unknown private fields, action ownership, platform
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 0ce5bfa2c4..5315612235 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -15,6 +15,18 @@
## Purpose
+### Memory presentation does not collect or renew observations
+
+Guest-memory provenance is projected from the already-owned snapshot in the
+existing drawer. A same-guest provenance change updates the Overview/History
+label without refetching metrics History, resetting the drawer, creating chart
+points or running a diagnostic. Fixed source labels and the original known
+time replace fabricated current freshness; there is no new timer, collector or
+fleet-wide query. Connected hook and mounted sparse/provenance controls pin
+unchanged numeric selection, source isolation and no-extra-History-read
+behaviour. Browser comparisons are source/render acceptance, not native CPU,
+RSS, backup safety or installed relief measurements.
+
### Guest read-state display does not start another read
The existing workload drawer's guest-read state and backup precaution derive
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index bf835e0494..0a0168e91a 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -21,6 +21,18 @@
## Purpose
+### Guest-memory read state stays independent of backup disk state
+
+The existing guest drawer retains Core's selected memory provenance through
+both resources-API and canonical snapshot consumers. Retained memory remains
+last-known at its original observation time, even after a new poll or cleared
+backup lock. Missing provenance is unknown, unavailable memory is not zero,
+and an independent current Proxmox/Pulse Agent memory reading remains current
+while guest-agent filesystem reads are deferred. History samples and its
+access-denial boundary remain authoritative. This client repair does not run
+backup/QGA operations or establish independent thaw, covered-filesystem writes,
+liveness, resumption or delivery. Existing outage precautions remain effective.
+
### Memory provenance does not imply recoverability
Canonical metric values may carry optional selected guest memory observation
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 4ad877c1d1..ae04260b9a 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -23,6 +23,32 @@ and sort the complete canonical change table while startup and ingestion wait.
## Purpose
+### Guest drawers consume selected memory provenance
+
+Existing guest drawers consume optional server-owned `memory.observation` and
+selected canonical/API metric annotations without renewing their original
+observation time from resource LastSeen, disk-read deferral or another platform
+facet. `useWorkloads`, `useUnifiedResources` and the legacy resource adapter
+preserve the selected annotation. A selected unannotated metric cannot borrow a
+conflicting raw platform annotation. Raw-memory fallback retains its own
+annotation only when its numeric reading is actually selected.
+
+Proxmox VM/LXC Overview states current, last known, unavailable or freshness
+unknown with the original known UTC time and an allowlisted source label.
+Missing, unrecognised or unusable provenance is not current. Unavailable usage
+keeps known capacity but never invents zero used/free/swap measurements. History
+keeps retained/unknown numeric context separate from current legends and stored
+points, dated inspection and denied source access. Same-guest source changes
+update without resetting the drawer or refetching History. Independent valid
+PVE/Pulse Agent readings remain current even while QGA disk reads are deferred;
+unannotated unrelated platforms keep existing presentation.
+
+Connected original-source/normalizer/adapter and mounted History controls plus
+`browser-tests/guest-memory-provenance.cjs` exercise these boundaries. They do
+not establish native thaw, covered-filesystem writes, liveness, reporter relief,
+publication or an installed containing release. No route, collection, numeric
+selection, retention, alert policy or diagnostic permission is added.
+
### Guest memory provenance follows metric selection
Raw Proxmox `memory.observation` preserves the guest builder's state, source
diff --git a/frontend-modern/browser-tests/guest-memory-provenance.cjs b/frontend-modern/browser-tests/guest-memory-provenance.cjs
new file mode 100644
index 0000000000..16f2b1b63b
--- /dev/null
+++ b/frontend-modern/browser-tests/guest-memory-provenance.cjs
@@ -0,0 +1,418 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const crypto = require('node:crypto');
+const { chromium, webkit } = require('playwright');
+const root = '/workspace/frontend-modern';
+const output = '/workspace/tmp/guest-memory-provenance';
+const origin = 'http://127.0.0.1:5329';
+const runtime = [
+ 'frontend-modern/src/types/api.ts',
+ 'frontend-modern/src/types/resource.ts',
+ 'frontend-modern/src/utils/memoryObservation.ts',
+ 'frontend-modern/src/utils/resourceStateAdapters.ts',
+ 'frontend-modern/src/hooks/useUnifiedResources.ts',
+ 'frontend-modern/src/hooks/useWorkloads.ts',
+ 'frontend-modern/src/components/Workloads/guestDrawerModel.ts',
+ 'frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx',
+ 'frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx',
+];
+const hash = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
+const initial = () => ({
+ state: 'last-known',
+ source: 'guest-agent-meminfo',
+ observedAt: '2026-10-04T14:00:00Z',
+ usage: 25,
+});
+const resource = (read) => {
+ const capacity = 4 * 1024 ** 3;
+ return {
+ id: read.peer ? 'fixture-b-pve1-101' : 'fixture-a-pve1-101',
+ type: 'vm',
+ name: read.peer ? 'peer-guest' : 'backup-guest',
+ status: 'online',
+ lastSeen: read.lastSeen ?? '2026-10-04T17:00:00Z',
+ sources: ['proxmox', 'agent'],
+ platformScopes: ['proxmox-pve'],
+ metrics: {
+ cpu: { percent: 10 },
+ ...(read.state === 'unavailable'
+ ? {}
+ : {
+ memory: {
+ percent: read.usage,
+ used: (capacity * read.usage) / 100,
+ total: capacity,
+ ...(read.state === undefined
+ ? {}
+ : {
+ observation: {
+ state: read.state,
+ source: read.source ?? '',
+ ...(read.observedAt ? { observedAt: read.observedAt } : {}),
+ },
+ }),
+ },
+ }),
+ disk: { percent: 50, total: capacity, used: capacity / 2 },
+ },
+ proxmox: {
+ vmid: 101,
+ nodeName: 'pve1',
+ instance: read.peer ? 'fixture-b' : 'fixture-a',
+ runtimeStatus: 'running',
+ guestAgentStatus: 'deferred',
+ guestAgentExpected: true,
+ diskStatusReason: 'prev-vm-locked',
+ lock: 'backup',
+ memory: {
+ total: capacity,
+ usageUnavailable: read.state === 'unavailable',
+ observation: { state: 'current', source: 'status-mem', observedAt: '2026-10-04T17:00:00Z' },
+ },
+ },
+ };
+};
+(async () => {
+ fs.mkdirSync(output, { recursive: true });
+ const result = {
+ result: 'incomplete',
+ playwright: require('playwright/package.json').version,
+ runtime_hashes: Object.fromEntries(runtime.map((p) => [p, hash('/workspace/' + p)])),
+ cases: [],
+ screenshots: [],
+ limits:
+ 'Production drawer/history/model with real raw, API, unified API and canonical full/delta/fast consumers. Synthetic source payloads, History replies and disconnected websocket metadata. No full App, native QGA/thaw, filesystem writes, physical phone, reporter, installed or publication acceptance.',
+ };
+ assert.equal(
+ result.playwright,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ process.chdir(root);
+ 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: output + '/cache',
+ server: { host: '127.0.0.1', port: 5329, strictPort: true },
+ });
+ let browser;
+ try {
+ await server.listen();
+ for (const [name, engine, width, dark] of [
+ ['desktop-light', chromium, 1365, false],
+ ['desktop-dark', chromium, 1365, true],
+ ['phone-dark', webkit, 390, true],
+ ['narrow-light', webkit, 320, false],
+ ]) {
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ for (const transport of [
+ 'raw',
+ 'api',
+ 'unified-api',
+ 'canonical-full',
+ 'canonical-delta',
+ 'canonical-fast',
+ ]) {
+ const page = await browser.newPage({
+ viewport: { width, height: width < 768 ? 844 : 900 },
+ isMobile: width < 768,
+ hasTouch: width < 768,
+ locale: 'en-GB',
+ timezoneId: 'UTC',
+ });
+ page.setDefaultTimeout(15000);
+ page.setDefaultNavigationTimeout(60000);
+ let read = initial(),
+ mode = 'empty';
+ const record = {
+ name,
+ transport,
+ width,
+ dark,
+ browser: browser.version(),
+ errors: [],
+ requests: [],
+ states: [],
+ };
+ result.cases.push(record);
+ page.on('pageerror', (e) => record.errors.push(e.message));
+ await page.route('**/*', async (route) => {
+ const url = new URL(route.request().url());
+ if (url.origin !== origin) {
+ record.requests.push({ blocked: true, origin: url.origin });
+ return route.abort();
+ }
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ record.requests.push({ path: url.pathname, method: route.request().method() });
+ assert.equal(route.request().method(), 'GET');
+ if (url.pathname === '/api/resources')
+ return route.fulfill({ json: { data: [resource(read)], meta: { totalPages: 1 } } });
+ if (url.pathname === '/api/metrics-store/history') {
+ if (mode === 'denied')
+ return route.fulfill({ status: 403, json: { error: 'private fixture refusal' } });
+ const start = Date.UTC(2026, 9, 4, 14),
+ end = Date.UTC(2026, 9, 4, 17),
+ point = (timestamp, value) => ({ timestamp, value, min: value, max: value });
+ return route.fulfill({
+ json: {
+ resourceType: url.searchParams.get('resourceType'),
+ resourceId: url.searchParams.get('resourceId'),
+ range: url.searchParams.get('range'),
+ start,
+ end,
+ metrics:
+ mode === 'stored'
+ ? {
+ memory: [point(start, 20), point(end, 30)],
+ cpu: [point(start, 10), point(end, 15)],
+ }
+ : {},
+ source: 'store',
+ },
+ });
+ }
+ return route.fulfill({
+ json:
+ url.pathname === '/api/license/runtime-capabilities'
+ ? {
+ capabilities: [],
+ limits: [],
+ max_history_days: 7,
+ hosted_mode: false,
+ runtime: { build: 'community' },
+ blocked_capabilities: [],
+ }
+ : { success: true, data: [], alerts: [], anomalies: [], config: null },
+ });
+ });
+ await page.addInitScript(
+ (value) =>
+ document.addEventListener('DOMContentLoaded', () => {
+ if (value) document.documentElement.classList.add('dark');
+ }),
+ dark,
+ );
+ await page.goto(
+ origin + '/browser-tests/guest-memory-provenance.html?transport=' + transport,
+ );
+ const drawer = page.getByRole('region', { name: 'Guest details' });
+ await drawer.getByText('Memory reading', { exact: true }).waitFor();
+ const overview = drawer.getByRole('tab', { name: 'Overview', exact: true });
+ const history = drawer.getByRole('tab', { name: 'History', exact: true });
+ const activate = async (control) => {
+ if (width < 768) await control.tap();
+ else {
+ await control.focus();
+ await page.keyboard.press('Enter');
+ }
+ };
+ // ObjectDrawerHeader deliberately extends its collapse hit area by
+ // four pixels into the surrounding padding. Its region scrollWidth is
+ // not a text-clipping measure. Check the actual reading cell and the
+ // page viewport instead, including retained and unknown long labels.
+ const checkReadingLayout = async (state) => {
+ const row = drawer.getByText('Memory reading', { exact: true }).locator('..');
+ const cell = row.locator('td').nth(1);
+ const layout = await cell.evaluate((element) => {
+ const text = element.querySelector('span');
+ const bounds = element.getBoundingClientRect();
+ const valueBounds = text.getBoundingClientRect();
+ const style = getComputedStyle(text);
+ return {
+ documentWidth: document.documentElement.clientWidth,
+ documentScrollWidth: document.documentElement.scrollWidth,
+ cell: { left: bounds.left, right: bounds.right },
+ value: {
+ left: valueBounds.left,
+ right: valueBounds.right,
+ width: text.clientWidth,
+ scrollWidth: text.scrollWidth,
+ whiteSpace: style.whiteSpace,
+ textOverflow: style.textOverflow,
+ },
+ };
+ });
+ (record.layouts ??= []).push({ state, ...layout });
+ assert(layout.documentScrollWidth <= layout.documentWidth + 1);
+ assert(layout.value.scrollWidth <= layout.value.width + 1);
+ assert(layout.value.left >= layout.cell.left - 1);
+ assert(layout.value.right <= layout.cell.right + 1);
+ assert.equal(layout.value.whiteSpace, 'normal');
+ assert.notEqual(layout.value.textOverflow, 'ellipsis');
+ };
+ const capture = async (state) => {
+ if (transport !== 'unified-api') return;
+ const file = `${name}-${state}.png`;
+ await drawer.screenshot({ path: output + '/' + file });
+ result.screenshots.push({
+ file,
+ sha256: hash(output + '/' + file),
+ name,
+ transport,
+ state,
+ });
+ };
+ const note = async (state) =>
+ record.states.push({
+ state,
+ evidence: await page.evaluate(() => window.__guestMemory.evidence()),
+ });
+ const update = async (next) => {
+ read = next;
+ await page.evaluate((value) => window.__guestMemory.update(value), next);
+ };
+ assert.match(
+ await drawer.innerText(),
+ /Last known · QEMU guest agent · 2026-10-04 14:00:00 UTC/,
+ );
+ await checkReadingLayout('retained-overview');
+ await capture('retained-overview');
+ await activate(history);
+ const chart = drawer.locator('[data-history-group="utilization"]');
+ await chart.locator('[data-history-last-known="memory"]').waitFor();
+ assert.equal(await chart.locator('[data-history-current="memory"]').count(), 0);
+ assert.match(
+ await chart.locator('[data-history-last-known="memory"]').innerText(),
+ /25\.0%.*last known/s,
+ );
+ assert.match(
+ await chart.locator('[data-history-deferred="memory"]').innerText(),
+ /14:00:00 UTC/,
+ );
+ assert.equal(await chart.locator('path').count(), 0);
+ const historyCalls = record.requests.filter(
+ (r) => r.path === '/api/metrics-store/history',
+ ).length;
+ await page.evaluate(() => {
+ window.__chartIdentity = document.querySelector('[data-history-group="utilization"]');
+ });
+ await note('retained');
+ await capture('retained-history');
+ await update({ ...initial(), lastSeen: '2026-10-04T18:00:00Z' });
+ assert.doesNotMatch(
+ await chart.locator('[data-history-deferred="memory"]').innerText(),
+ /18:00:00/,
+ );
+ await update({
+ state: 'current',
+ source: 'status-mem',
+ observedAt: '2026-10-04T17:00:00Z',
+ usage: 35,
+ });
+ await chart.locator('[data-history-current="memory"]').waitFor();
+ assert.match(
+ await chart.locator('[data-history-current="memory"]').innerText(),
+ /35\.0%.*current/s,
+ );
+ assert.equal(await chart.locator('[data-history-deferred="memory"]').count(), 0);
+ assert.equal(await chart.locator('[data-history-last-known="disk"]').count(), 1);
+ await note('independent-current-pve');
+ await update({ state: 'unavailable', source: 'unavailable', usage: 0 });
+ assert.equal(await chart.locator('[data-history-current="memory"]').count(), 0);
+ assert.equal(await chart.locator('[data-history-last-known="memory"]').count(), 0);
+ assert.match(await chart.innerText(), /Memory\s*-/);
+ await note('unavailable');
+ await update({ usage: 40 });
+ await chart.locator('[data-history-unknown="memory"]').waitFor();
+ assert.match(
+ await chart.locator('[data-history-unknown="memory"]').innerText(),
+ /40\.0%.*freshness unknown/s,
+ );
+ assert.equal(await chart.locator('[data-history-current="memory"]').count(), 0);
+ assert.equal(await chart.locator('[data-history-last-known="memory"]').count(), 0);
+ assert.equal(
+ await page.evaluate(
+ () =>
+ window.__chartIdentity ===
+ document.querySelector('[data-history-group="utilization"]'),
+ ),
+ true,
+ );
+ assert.equal(
+ record.requests.filter((r) => r.path === '/api/metrics-store/history').length,
+ historyCalls,
+ );
+ await activate(overview);
+ assert.match(await drawer.innerText(), /Freshness unknown · Unknown source · time unknown/);
+ await checkReadingLayout('unknown-overview');
+ await capture('unknown-overview');
+ await note('legacy-unknown');
+ await activate(history);
+ await update({ state: 'last-known', source: 'private provider detail', usage: 0 });
+ await chart.locator('[data-history-last-known="memory"]').waitFor();
+ assert.match(
+ await chart.locator('[data-history-last-known="memory"]').innerText(),
+ /0\.0%.*last known/s,
+ );
+ assert.match(
+ await chart.locator('[data-history-deferred="memory"]').innerText(),
+ /Observation time unknown/,
+ );
+ assert.doesNotMatch(await drawer.innerText(), /private provider detail/);
+ await note('retained-zero-unknown-age');
+ mode = 'stored';
+ await activate(drawer.getByRole('button', { name: 'Refresh history', exact: true }));
+ await page.waitForFunction(
+ () => document.querySelectorAll('[data-history-group="utilization"] path').length === 2,
+ );
+ assert.equal(await chart.locator('[data-history-current="memory"]').count(), 0);
+ assert.equal(await chart.locator('[data-history-last-known="memory"]').count(), 0);
+ assert.match(await chart.innerText(), /Memory\s*30\.0%/);
+ const slider = chart.getByRole('slider');
+ await slider.focus();
+ await page.keyboard.press('Home');
+ assert.match(await slider.getAttribute('aria-valuetext'), /Memory 20\.0%/);
+ await note('stored-keyboard-inspection');
+ mode = 'denied';
+ await activate(drawer.getByRole('button', { name: 'Refresh history', exact: true }));
+ await drawer.getByRole('button', { name: 'Retry history', exact: true }).waitFor();
+ assert.equal(await drawer.locator('[data-history-deferred]').count(), 0);
+ assert.equal(await drawer.locator('[data-history-group]').count(), 0);
+ assert.doesNotMatch(await drawer.innerText(), /private fixture refusal/);
+ mode = 'empty';
+ await activate(drawer.getByRole('button', { name: 'Retry history', exact: true }));
+ await chart.locator('[data-history-last-known="memory"]').waitFor();
+ await note('denied-and-recovered');
+ await update({
+ peer: true,
+ state: 'current',
+ source: 'agent',
+ observedAt: '2026-10-04T16:00:00Z',
+ usage: 70,
+ });
+ await drawer.getByText('peer-guest', { exact: true }).waitFor();
+ assert.equal(await overview.getAttribute('aria-selected'), 'true');
+ assert.match(await drawer.innerText(), /Current · Pulse Agent · 2026-10-04 16:00:00 UTC/);
+ assert.doesNotMatch(await drawer.innerText(), /14:00:00 UTC/);
+ await note('peer-identity-replacement');
+ await checkReadingLayout('peer-overview');
+ assert.deepEqual(record.errors, []);
+ assert(record.requests.every((r) => !r.blocked && r.method === 'GET'));
+ await page.close();
+ }
+ await browser.close();
+ browser = undefined;
+ }
+ result.result = 'passed';
+ fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
+ process.stdout.write(
+ `Guest memory provenance: ${result.cases.length} transport/layout cases passed.\n`,
+ );
+ } catch (error) {
+ fs.writeFileSync(
+ output + '/failure.json',
+ JSON.stringify({ error: String(error), stack: error.stack, result }, null, 2) + '\n',
+ );
+ throw error;
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})();
diff --git a/frontend-modern/browser-tests/guest-memory-provenance.html b/frontend-modern/browser-tests/guest-memory-provenance.html
new file mode 100644
index 0000000000..7a8916a1c3
--- /dev/null
+++ b/frontend-modern/browser-tests/guest-memory-provenance.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Guest memory provenance evidence
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/guest-memory-provenance.tsx b/frontend-modern/browser-tests/guest-memory-provenance.tsx
new file mode 100644
index 0000000000..a33edaeac4
--- /dev/null
+++ b/frontend-modern/browser-tests/guest-memory-provenance.tsx
@@ -0,0 +1,213 @@
+// Production hook/adapter/drawer consumers, synthetic source snapshots only.
+import { createSignal, Show } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
+import { useWorkloads } from '../src/hooks/useWorkloads';
+import { useUnifiedResources } from '../src/hooks/useUnifiedResources';
+import {
+ mergeCanonicalResourceSnapshot,
+ mergeCanonicalResourceDeltaSnapshot,
+} from '../src/utils/resourceStateAdapters';
+import { GuestDrawer } from '../src/components/Workloads/GuestDrawer';
+import { DarkModeContext } from '../src/contexts/appRuntime';
+import { syncAIRuntimeSettings } from '../src/stores/aiRuntimeState';
+import type { Resource } from '../src/types/resource';
+import type { MemoryObservation } from '../src/types/api';
+import type { WorkloadGuest } from '../src/types/workloads';
+import '../src/index.css';
+
+syncAIRuntimeSettings({ discovery_enabled: false });
+interface Reading {
+ state?: string;
+ source?: string;
+ observedAt?: string;
+ usage: number;
+ lastSeen?: string;
+ peer?: boolean;
+}
+const initial: Reading = {
+ state: 'last-known',
+ source: 'guest-agent-meminfo',
+ observedAt: '2026-10-04T14:00:00Z',
+ usage: 25,
+};
+const observation = (read: Reading): MemoryObservation | undefined =>
+ read.state === undefined
+ ? undefined
+ : {
+ state: read.state,
+ source: read.source ?? '',
+ ...(read.observedAt ? { observedAt: read.observedAt } : {}),
+ };
+const capacity = 4 * 1024 ** 3;
+const id = (read: Reading) => (read.peer ? 'fixture-b-pve1-101' : 'fixture-a-pve1-101');
+const nativeResource = (read: Reading): Resource => ({
+ id: id(read),
+ type: 'vm',
+ name: read.peer ? 'peer-guest' : 'backup-guest',
+ displayName: read.peer ? 'peer-guest' : 'backup-guest',
+ status: 'online',
+ sources: ['proxmox', 'agent'],
+ platformType: 'proxmox-pve',
+ platformScopes: ['proxmox-pve'],
+ sourceType: 'hybrid',
+ lastSeen: Date.parse(read.lastSeen ?? '2026-10-04T17:00:00Z'),
+ cpu: { current: 10 },
+ memory:
+ read.state === 'unavailable'
+ ? undefined
+ : {
+ current: read.usage,
+ used: (capacity * read.usage) / 100,
+ total: capacity,
+ ...(observation(read) ? { observation: observation(read) } : {}),
+ },
+ disk: { current: 50, used: capacity / 2, total: capacity },
+ proxmox: {
+ vmid: 101,
+ nodeName: 'pve1',
+ instance: read.peer ? 'fixture-b' : 'fixture-a',
+ runtimeStatus: 'running',
+ guestAgentStatus: 'deferred',
+ guestAgentExpected: true,
+ diskStatusReason: 'prev-vm-locked',
+ lock: 'backup',
+ memory: {
+ total: capacity,
+ used: capacity / 2,
+ free: capacity / 2,
+ usage: 50,
+ usageUnavailable: read.state === 'unavailable',
+ observation: { state: 'current', source: 'status-mem', observedAt: '2026-10-04T17:00:00Z' },
+ },
+ },
+});
+const rawGuest = (read: Reading): WorkloadGuest => ({
+ id: id(read),
+ vmid: 101,
+ name: read.peer ? 'peer-guest' : 'backup-guest',
+ instance: read.peer ? 'fixture-b' : 'fixture-a',
+ node: 'pve1',
+ type: 'qemu',
+ status: 'running',
+ cpu: 0.1,
+ cpus: 2,
+ memory: {
+ total: capacity,
+ used: (capacity * read.usage) / 100,
+ free: (capacity * (100 - read.usage)) / 100,
+ usage: read.usage,
+ usageUnavailable: read.state === 'unavailable',
+ ...(observation(read) ? { observation: observation(read) } : {}),
+ },
+ disk: { usage: 50, total: capacity, used: capacity / 2 },
+ diskStatusReason: 'prev-vm-locked',
+ lock: 'backup',
+ guestAgentStatus: 'deferred',
+ guestAgentExpected: true,
+ networkIn: 10,
+ networkOut: 20,
+ diskRead: 30,
+ diskWrite: 40,
+ uptime: 3600,
+ template: false,
+ lastBackup: 0,
+ lastSeen: read.lastSeen ?? '2026-10-04T17:00:00Z',
+ tags: [],
+});
+// Only the websocket transport is synthetic. API conversion and owned hooks
+// remain production code, with real requests fulfilled by the bounded script.
+window.__pulseWsStore = {
+ state: { resources: [], activeAlerts: [], recentlyResolved: [] },
+ connected: () => false,
+ initialDataReceived: () => true,
+ resourceSnapshotReceived: () => false,
+ resourceChange: () => ({ version: 0, changedIds: null, changedKeys: null }),
+ changedResourceMetaSince: () => null,
+ changedResourceIdsSince: () => null,
+ shutdown: () => {},
+} as unknown as NonNullable;
+function Fixture() {
+ const transport = new URLSearchParams(window.location.search).get('transport') ?? 'api';
+ const [read, setRead] = createSignal(initial);
+ const [snapshot, setSnapshot] = createSignal([nativeResource(initial)]);
+ const unified =
+ transport === 'unified-api'
+ ? useUnifiedResources({ query: 'type=vm', realtimeEnabled: () => false })
+ : null;
+ const workloads =
+ transport === 'raw'
+ ? null
+ : useWorkloads(
+ () => true,
+ transport === 'api'
+ ? {}
+ : { resourceSnapshot: unified ? () => unified.resources() : snapshot },
+ );
+ const guest = () => (transport === 'raw' ? rawGuest(read()) : workloads!.workloads()[0]);
+ (window as any).__guestMemory = {
+ update: async (next: Reading) => {
+ setRead(next);
+ if (transport === 'raw') return;
+ if (transport === 'api') {
+ await workloads!.refetch();
+ return;
+ }
+ if (unified) {
+ await unified.refetch();
+ return;
+ }
+ const incoming = nativeResource(next);
+ setSnapshot((previous) =>
+ transport === 'canonical-full' || next.peer
+ ? mergeCanonicalResourceSnapshot([incoming], previous)
+ : mergeCanonicalResourceDeltaSnapshot(
+ [incoming],
+ previous,
+ new Set([incoming.id]),
+ transport === 'canonical-fast'
+ ? new Map([[incoming.id, ['memory', 'proxmox', 'lastSeen']]])
+ : undefined,
+ ),
+ );
+ },
+ evidence: () => ({
+ id: guest()?.id,
+ observation: guest()?.memory.observation,
+ usage: guest()?.memory.usage,
+ unavailable: guest()?.memory.usageUnavailable,
+ }),
+ };
+ return (
+
+ Guest memory evidence
+
+ Synthetic {transport} observations. No guest checks are sent.
+
+
+ {() => (
+
+ )}
+
+
+ );
+}
+render(
+ () => (
+
+ (
+ document.documentElement.classList.contains('dark')}
+ >
+
+
+ )}
+ />
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-tests/system-alert-scope.cjs b/frontend-modern/browser-tests/system-alert-scope.cjs
new file mode 100644
index 0000000000..a91ae3d078
--- /dev/null
+++ b/frontend-modern/browser-tests/system-alert-scope.cjs
@@ -0,0 +1,378 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const crypto = require('node:crypto');
+const { chromium, webkit } = require('playwright');
+const root = '/workspace/frontend-modern';
+const output = '/workspace/tmp/system-alert-scope-proof';
+const runtime = [
+ 'frontend-modern/src/utils/alertScope.ts',
+ 'frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts',
+ 'frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx',
+ 'frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx',
+ 'frontend-modern/src/features/alerts/alertHistoryModel.ts',
+ 'frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx',
+];
+const systemTypes = [
+ 'backup-evaluation',
+ 'notification-delivery',
+ 'deadman-delivery',
+ 'deadman-monitoring-stalled',
+ 'deadman-interruption',
+ 'deadman-state',
+];
+const legacy = {
+ id: 'retained-legacy-system-id',
+ type: 'notification-delivery',
+ level: 'warning',
+ resourceId: 'vm-wrong',
+ resourceName: 'Pulse',
+ node: 'wrong-node',
+ metadata: { systemAlert: true, resourceType: 'vm' },
+ message: 'Synthetic retained notification delivery outage.',
+ startTime: '2026-10-04T14:00:00Z',
+ lastSeen: '2026-10-04T14:30:00Z',
+ acknowledged: true,
+ value: 0,
+ threshold: 0,
+};
+const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
+(async () => {
+ fs.mkdirSync(output, { recursive: true });
+ const version = require('playwright/package.json').version;
+ assert.equal(
+ version,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ const hashes = Object.fromEntries(
+ runtime.map((file) => [file, hash(path.join('/workspace', file))]),
+ );
+ process.chdir(root);
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ root,
+ configFile: path.join(root, 'vite.config.ts'),
+ cacheDir: path.join(output, 'vite-cache'),
+ server: { host: '127.0.0.1', port: 5297, strictPort: true, watch: null },
+ });
+ const origin = 'http://127.0.0.1:5297';
+ const results = [],
+ screenshots = [];
+ let browser,
+ phase = 'server';
+ try {
+ await server.listen();
+ for (const [name, engine, width, dark] of [
+ ['chromium-desktop-light', chromium, 1365, false],
+ ['chromium-desktop-dark', chromium, 1365, true],
+ ['webkit-phone-dark', webkit, 390, true],
+ ['webkit-narrow-light', webkit, 320, false],
+ ]) {
+ phase = name;
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({
+ viewport: { width, height: width < 500 ? 844 : 900 },
+ isMobile: width < 500,
+ hasTouch: width < 500,
+ locale: 'en-GB',
+ timezoneId: 'UTC',
+ });
+ page.setDefaultTimeout(10000);
+ page.setDefaultNavigationTimeout(60000);
+ const errors = [],
+ requests = [],
+ offOrigin = [],
+ writes = [],
+ checks = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.addInitScript((dark) => {
+ localStorage.clear();
+ const apply = () => document.documentElement.classList.toggle('dark', dark);
+ if (document.documentElement) apply();
+ else document.addEventListener('DOMContentLoaded', apply, { once: true });
+ }, dark);
+ await page.routeWebSocket(/.*/, (socket) => socket.close());
+ await page.route('**/*', async (route) => {
+ const req = route.request(),
+ url = new URL(req.url());
+ if (url.origin !== origin) {
+ offOrigin.push(url.origin);
+ return route.abort();
+ }
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ requests.push({ path: url.pathname, query: url.search, method: req.method() });
+ if (req.method() !== 'GET') {
+ writes.push({ path: url.pathname, method: req.method(), body: req.postDataJSON() });
+ assert.ok(
+ ['/api/alerts/acknowledge', '/api/alerts/unacknowledge'].includes(url.pathname),
+ 'unexpected write',
+ );
+ assert.equal(req.method(), 'POST');
+ assert.deepEqual(req.postDataJSON(), {
+ alertIdentifier: 'pulse-system-backup-evaluation',
+ });
+ return route.fulfill({ json: { success: true } });
+ }
+ if (url.pathname === '/api/alerts/history') return route.fulfill({ json: [legacy] });
+ if (url.pathname === '/api/alerts/incidents') return route.fulfill({ json: null });
+ if (url.pathname === '/api/notifications/health')
+ return route.fulfill({
+ json: {
+ overall_healthy: true,
+ queue: {
+ healthy: true,
+ status: 'healthy',
+ pending: 0,
+ sending: 0,
+ sent: 0,
+ failed: 0,
+ dlq: 0,
+ attention_required: 0,
+ reason_codes: [],
+ completed_retention_days: 7,
+ dead_letter_retention_days: 30,
+ counts_are_retention_bounded: true,
+ retry_attempts_affect_health: false,
+ terminal_failures_affect_health: true,
+ failure_classes_7d: {
+ authentication: 0,
+ rate_limited: 0,
+ connectivity: 0,
+ tls: 0,
+ configuration: 0,
+ rejected: 0,
+ server_error: 0,
+ unknown: 0,
+ },
+ failure_class_window_days: 7,
+ failure_classes_available: true,
+ },
+ },
+ });
+ if (url.pathname.includes('license'))
+ return route.fulfill({
+ json: { capabilities: [], entitlements: [], limits: [], max_history_days: 90 },
+ });
+ if (url.pathname === '/api/security/status')
+ return route.fulfill({
+ json: {
+ hasAuthentication: true,
+ requiresAuth: false,
+ sessionCapabilities: { assistantEnabled: true },
+ },
+ });
+ return route.fulfill({ json: [] });
+ });
+ const check = async (label, action) => {
+ phase = `${name}: ${label}`;
+ await action();
+ checks.push({ label, passed: true });
+ };
+ const capture = async (locator, state) => {
+ const file = `${name}-${state}.png`;
+ await locator.screenshot({ path: path.join(output, file) });
+ screenshots.push({ file, state, width, dark, sha256: hash(path.join(output, file)) });
+ };
+ const assertSystemContext = async (id) => {
+ const context = await page.evaluate(() => window.__systemAlertScope.context());
+ assert.equal(context.targetId, undefined);
+ assert.equal(context.targetType, undefined);
+ assert.deepEqual(context.handoffResources, []);
+ assert.equal(context.autonomousMode, false);
+ assert.equal(context.context.guestName, undefined);
+ assert.equal(context.context.node, undefined);
+ assert.match(context.handoffContext, /Scope: Pulse itself \(not a monitored resource\)/);
+ assert.ok(context.handoffContext.includes(`Alert Identifier: ${id}`));
+ assert.ok(context.handoffContext.includes('Operator Boundary:'));
+ assert.doesNotMatch(
+ context.handoffContext,
+ /Current Value:|Threshold:|wrong-node|vm-wrong/,
+ );
+ assert.doesNotMatch(context.briefing.detailLines.join('\n'), /Current value|threshold/);
+ await page.evaluate(() => window.__systemAlertScope.closeExplanation());
+ };
+ try {
+ await page.goto(`${origin}/browser-tests/system-alert-scope.html`);
+ const overview = page.getByRole('region', { name: 'Existing alert overview' });
+ const history = page.getByRole('region', { name: 'Existing alert history' });
+ const backupCard = page.locator('#alert-pulse-system-backup-evaluation');
+ await check(
+ 'canonical service warning has no false resource link or policy action',
+ async () => {
+ await backupCard.waitFor({ state: 'visible' });
+ assert.equal(
+ await overview
+ .getByText('Notification delivery status is unavailable', { exact: true })
+ .count(),
+ 0,
+ );
+ assert.equal(await backupCard.getByRole('link').count(), 0);
+ assert.equal(
+ await backupCard.getByRole('button', { name: 'Have Patrol investigate' }).count(),
+ 0,
+ );
+ assert.match(await backupCard.textContent(), /Backup-age alerts were not evaluated/);
+ await page.waitForFunction(() => window.__systemAlertScope.historyRows().length === 3);
+ const rows = await page.evaluate(() => window.__systemAlertScope.historyRows());
+ for (const row of rows.filter((r) => r.systemAlert)) {
+ assert.equal(row.resourceType, 'Pulse');
+ assert.equal(row.resourceId, '');
+ }
+ assert.equal(rows.find((r) => r.id === 'resource-cpu').resourceType, 'vm');
+ },
+ );
+ await capture(overview, 'backup-warning');
+ await check('acknowledgement remains active, not recovery', async () => {
+ await backupCard.getByRole('button', { name: 'Acknowledge', exact: true }).click();
+ await backupCard.getByText('Acknowledged', { exact: true }).waitFor();
+ assert.match(await backupCard.textContent(), /Backup-age alerts were not evaluated/);
+ await backupCard.getByRole('button', { name: 'Unacknowledge', exact: true }).click();
+ await backupCard.getByRole('button', { name: 'Acknowledge', exact: true }).waitFor();
+ });
+ await check('occurrence timeline keeps exact service identity', async () => {
+ await backupCard.getByRole('button', { name: 'Timeline', exact: true }).click();
+ await page.waitForFunction(() =>
+ document
+ .querySelector('#alert-pulse-system-backup-evaluation')
+ ?.textContent.includes('No incident'),
+ );
+ const incident = requests.find((r) => r.path === '/api/alerts/incidents');
+ assert.ok(incident);
+ const query = new URLSearchParams(incident.query);
+ assert.equal(query.get('alertIdentifier'), 'pulse-system-backup-evaluation');
+ assert.equal(query.get('started_at'), '2026-10-04T15:00:00Z');
+ await backupCard.getByRole('button', { name: 'Hide Timeline', exact: true }).click();
+ });
+ for (const type of systemTypes) {
+ await check(`${type} explanation is service-scoped and model-only`, async () => {
+ await page.evaluate((type) => window.__systemAlertScope.updateSystem(type), type);
+ const card = page.locator(`#alert-pulse-system-${type}`);
+ await card.waitFor({ state: 'visible' });
+ assert.equal(await card.getByRole('link').count(), 0);
+ const button = card.getByRole('button', { name: 'Ask Pulse Assistant', exact: true });
+ if (width > 500) {
+ await button.focus();
+ await page.keyboard.press('Enter');
+ } else await button.tap();
+ await assertSystemContext(`pulse-system-${type}`);
+ });
+ }
+ await check(
+ 'legacy system metadata wins over conflicting resource/metric hints',
+ async () => {
+ await page.evaluate(() =>
+ window.__systemAlertScope.updateSystem('future-condition', true),
+ );
+ const card = page.locator('#alert-legacy-system-id');
+ await card.waitFor({ state: 'visible' });
+ assert.equal(await card.getByRole('link').count(), 0);
+ assert.doesNotMatch(await card.textContent(), /limit:|wrong-node/);
+ await card.getByRole('button', { name: 'Ask Pulse Assistant', exact: true }).click();
+ await assertSystemContext('legacy-system-id');
+ },
+ );
+ await check(
+ 'desktop and phone History reconstruct the retained service marker',
+ async () => {
+ const row =
+ width < 500
+ ? history
+ .locator('[data-alert-history-row-key]')
+ .filter({ hasText: legacy.message })
+ : history.getByRole('row').filter({ hasText: legacy.message });
+ assert.equal(await row.locator('[data-alert-history-action="resource"]').count(), 0);
+ await row.getByRole('button', { name: 'Ask Pulse Assistant about this alert' }).click();
+ await assertSystemContext(legacy.id);
+ },
+ );
+ await capture(history, 'retained-history');
+ await check(
+ 'monitored VM called Pulse retains its link and targeted affordances',
+ async () => {
+ const card = page.locator('#alert-resource-cpu');
+ assert.equal(
+ await card.getByRole('link', { name: 'Pulse', exact: true }).getAttribute('href'),
+ '/proxmox/overview',
+ );
+ assert.match(await card.textContent(), /limit: 80%/);
+ await card
+ .getByRole('button', { name: 'Have Patrol investigate' })
+ .waitFor({ state: 'visible' });
+ },
+ );
+ await check(
+ 'authoritative recovery withdraws only the current service warning',
+ async () => {
+ await page.evaluate(() => window.__systemAlertScope.recover());
+ assert.equal(await overview.locator('[id^="alert-legacy"]').count(), 0);
+ assert.equal(await overview.locator('#alert-resource-cpu').count(), 1);
+ assert.ok(
+ (await page.evaluate(() => window.__systemAlertScope.historyRows())).some(
+ (r) => r.id === legacy.id,
+ ),
+ );
+ },
+ );
+ assert.deepEqual(errors, []);
+ assert.deepEqual(offOrigin, []);
+ assert.equal(writes.length, 2);
+ assert.ok(
+ !requests.some((r) => /patrol\/run|execute|diagnostic|ai\/chat|guest-agent/.test(r.path)),
+ );
+ results.push({
+ name,
+ width,
+ dark,
+ browser_version: browser.version(),
+ checks,
+ requests,
+ writes,
+ errors,
+ offOrigin,
+ });
+ } finally {
+ await browser.close();
+ browser = null;
+ }
+ }
+ fs.writeFileSync(
+ path.join(output, 'result.json'),
+ JSON.stringify(
+ { passed: true, playwright_version: version, content_sha256: hashes, results, screenshots },
+ null,
+ 2,
+ ) + '\n',
+ );
+ console.log(
+ JSON.stringify({
+ passed: true,
+ groups: results.length,
+ checks: results.reduce((n, r) => n + r.checks.length, 0),
+ screenshots: screenshots.length,
+ playwright_version: version,
+ }),
+ );
+ } catch (error) {
+ fs.writeFileSync(
+ path.join(output, 'failed.json'),
+ JSON.stringify(
+ { passed: false, phase, error: error.message, results, screenshots },
+ null,
+ 2,
+ ) + '\n',
+ );
+ throw error;
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/system-alert-scope.html b/frontend-modern/browser-tests/system-alert-scope.html
new file mode 100644
index 0000000000..5538407dca
--- /dev/null
+++ b/frontend-modern/browser-tests/system-alert-scope.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ System-alert scope verification
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/system-alert-scope.tsx b/frontend-modern/browser-tests/system-alert-scope.tsx
new file mode 100644
index 0000000000..ca02744e78
--- /dev/null
+++ b/frontend-modern/browser-tests/system-alert-scope.tsx
@@ -0,0 +1,107 @@
+// Production alert components/state/router and real Assistant handoff store.
+// Synthetic API evidence; no native service, model, diagnostic or command.
+import { createSignal, Show } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
+import { OverviewTab } from '../src/features/alerts/OverviewTab';
+import { AlertHistoryTableSection } from '../src/features/alerts/AlertHistoryTableSection';
+import { useAlertHistoryState } from '../src/features/alerts/useAlertHistoryState';
+import { makeSystemAlert } from '../src/features/alerts/__fixtures__/systemAlerts';
+import { aiChatStore } from '../src/stores/aiChat';
+import type { Alert } from '../src/types/api';
+import type { Resource } from '../src/types/resource';
+import '../src/index.css';
+
+aiChatStore.setEnabled(true);
+const pulseVM = { id: 'vm-pulse', name: 'Pulse', type: 'vm' } as Resource;
+const control = makeSystemAlert('cpu', {
+ id: 'resource-cpu',
+ resourceId: pulseVM.id,
+ metadata: { resourceType: 'vm' },
+ value: 92,
+ threshold: 80,
+ message: 'Synthetic CPU reading on the monitored VM called Pulse.',
+});
+
+function Fixture() {
+ const initial = makeSystemAlert();
+ const [alerts, setAlerts] = createSignal>({
+ [initial.id]: initial,
+ [control.id]: control,
+ });
+ const [showAcknowledged, setShowAcknowledged] = createSignal(true);
+ const history = useAlertHistoryState({
+ activeAlerts: alerts,
+ getResource: () => pulseVM,
+ allResources: () => [pulseVM],
+ });
+ (window as any).__systemAlertScope = {
+ updateSystem: (type: string, legacy = false) => {
+ const next = makeSystemAlert(
+ type,
+ legacy
+ ? {
+ id: 'legacy-system-id',
+ resourceId: 'vm-wrong',
+ node: 'wrong-node',
+ metadata: { systemAlert: true, resourceType: 'vm' },
+ threshold: 80,
+ }
+ : {},
+ );
+ setAlerts({ [next.id]: next, [control.id]: control });
+ },
+ recover: () => setAlerts({ [control.id]: control }),
+ alerts,
+ historyRows: history.alertData,
+ context: () => aiChatStore.context,
+ closeExplanation: () => aiChatStore.close(),
+ };
+ return (
+
+ Pulse system-alert evidence
+
+ Synthetic observations. No workload, diagnostic or model is contacted.
+
+
+
+ setAlerts((previous) => ({ ...previous, [id]: { ...previous[id], ...updates } }))
+ }
+ showAcknowledged={showAcknowledged}
+ setShowAcknowledged={setShowAcknowledged}
+ showQuickTip={() => false}
+ dismissQuickTip={() => {}}
+ alertsDisabled={() => false}
+ />
+
+
+
+
+
+ Test capture of the real explanation store; no Assistant request is executed.
+
+
+ {JSON.stringify(aiChatStore.context, null, 2)}
+
+
+
+
+ );
+}
+render(
+ () => (
+
+
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification/4c16797de9c711c2.json b/frontend-modern/browser-verification/4c16797de9c711c2.json
new file mode 100644
index 0000000000..196b87c2f8
--- /dev/null
+++ b/frontend-modern/browser-verification/4c16797de9c711c2.json
@@ -0,0 +1,54 @@
+{
+ "version": 1,
+ "base_sha": "63443e8712dbb6d3866bdc5fce9f2b89c13be51c",
+ "verified_at": "2026-10-04T16:33:49Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx",
+ "frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts",
+ "frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx",
+ "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx",
+ "frontend-modern/src/features/alerts/alertHistoryModel.ts",
+ "frontend-modern/src/utils/alertScope.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx": "f68c7c887abd83ee4b8c022874951b6be06d5b9975bb2a78a0f782d3d5419001",
+ "frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts": "01eaea04cf8730fca4342d12b2be4e2fefd6e31b530ab1e416a937557cb4d664",
+ "frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx": "9e7bf38b1f3e57f4a8b178ee48770561e5eef7f93440c190d3001ed95526e765",
+ "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx": "198eecb8d3f94aaf14020686c494f343db687d49c0933589005adb795b35bc8b",
+ "frontend-modern/src/features/alerts/alertHistoryModel.ts": "69e754d21e4b700bd8e7308e72082786373b1f25f0e2d4ea4434a2d4ba804a1a",
+ "frontend-modern/src/utils/alertScope.ts": "b10844bcf4a3a2a829d49c385369a0559102477b84688f27c311de02ce931065"
+ },
+ "routes": ["/browser-tests/system-alert-scope.html"],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ },
+ {
+ "width": 320,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Production Overview and desktop/mobile History with synthetic canonical system-alert payloads; no native service or full-App acceptance.",
+ "Backup evaluation unavailable, all six current system types, metadata-only future/legacy type with conflicting resource/metric hints.",
+ "Monitored VM named Pulse remains resource scoped; independent synthetic notification queue health is complete and healthy.",
+ "Acknowledged service warning remains active; explicit source recovery removes only current service warning and retains historical occurrence.",
+ "Light/dark desktop and 390px/320px WebKit touch layouts; eight final screenshots inspected."
+ ],
+ "interactions": [
+ "Acknowledgement and unacknowledgement retain exact canonical service alert identifier and do not resolve its condition.",
+ "Occurrence timeline fetch preserves service identifier and original start timestamp.",
+ "Six real explanation-button handoffs use keyboard Enter on desktop and touch tap on phones; no metric values, resource targets, Patrol, model or diagnostic calls.",
+ "History explanation reconstructs metadata-only system scope; resource link/Monitoring/Patrol remain only on actual VM control."
+ ],
+ "browser_command": "pulse-worker-browser frontend-modern/browser-tests/system-alert-scope.cjs",
+ "playwright_version": "1.56.1",
+ "passed_groups": 4,
+ "passed_checks": 52
+}
diff --git a/frontend-modern/browser-verification/aca8311c65fac54f.json b/frontend-modern/browser-verification/aca8311c65fac54f.json
new file mode 100644
index 0000000000..6beb5131a5
--- /dev/null
+++ b/frontend-modern/browser-verification/aca8311c65fac54f.json
@@ -0,0 +1,77 @@
+{
+ "version": 1,
+ "result": "passed",
+ "base_sha": "3d846f76d724b1853a3ea58201cfc6eb7e25b084",
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx",
+ "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
+ "frontend-modern/src/components/Workloads/guestDrawerModel.ts",
+ "frontend-modern/src/hooks/useUnifiedResources.ts",
+ "frontend-modern/src/hooks/useWorkloads.ts",
+ "frontend-modern/src/types/api.ts",
+ "frontend-modern/src/types/resource.ts",
+ "frontend-modern/src/utils/memoryObservation.ts",
+ "frontend-modern/src/utils/resourceStateAdapters.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/types/api.ts": "f5805a599793431a45c5a88341cc8af4ad1a05787ce51d75b9fbb75f5dcf5606",
+ "frontend-modern/src/types/resource.ts": "daffee073bb11699518d9c45aeb8075d4e53535ee6f385b0dffb8d06a05c670d",
+ "frontend-modern/src/utils/memoryObservation.ts": "a9ef178ff58cf9ab3e622af3a6cceb9e9da9f42e03b2d246fe73ab22e8a3bf1e",
+ "frontend-modern/src/utils/resourceStateAdapters.ts": "f55c5ff10daafad9b6c577f68777696329c69e0b0bb2c13ec662c7c7d59e4e29",
+ "frontend-modern/src/hooks/useUnifiedResources.ts": "e4ea180cd2e372eab77414ae96d9c180ad706b67b2619a67f29bb046e38a55ef",
+ "frontend-modern/src/hooks/useWorkloads.ts": "0915943dd0cbb65940ab75a3e1143a80eda53c32db485181d56948e640f9e66d",
+ "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "c4e4d5f2ecd0d65cba73b14d400af0f8eacae10965d1ad9b499dfc1b03ac658d",
+ "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "2c72e3b40e311c91ee789724ac72cde01ebe36166c1f23a462a320d1a7f03add",
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "5d592a9c3422a20e4596d25dc673f894219663c80392240ea1094ccd282e0848"
+ },
+ "routes": [
+ "/browser-tests/guest-memory-provenance.html?transport=raw",
+ "/browser-tests/guest-memory-provenance.html?transport=api",
+ "/browser-tests/guest-memory-provenance.html?transport=unified-api",
+ "/browser-tests/guest-memory-provenance.html?transport=canonical-full",
+ "/browser-tests/guest-memory-provenance.html?transport=canonical-delta",
+ "/browser-tests/guest-memory-provenance.html?transport=canonical-fast"
+ ],
+ "states": [
+ "retained QGA memory with original observation time",
+ "repeated deferral without renewing age or History reads",
+ "independent current PVE memory despite disk deferral",
+ "unavailable memory without fabricated zero/free measurement",
+ "legacy unknown memory without borrowing conflicting platform provenance",
+ "retained measured zero with unknown source/time",
+ "stored points and dated keyboard inspection",
+ "403 concealment followed by permitted recovery",
+ "same-guest replacement without chart remount; peer identity replacement resets drawer"
+ ],
+ "interactions": [
+ "keyboard Enter and touch select real drawer tabs and Refresh/Retry",
+ "Home key inspects stored history through the accessible slider",
+ "production raw/API/unified/canonical full/delta/fast consumer updates",
+ "reading-cell wrapping and page-width checks at all viewports",
+ "only bounded synthetic GET responses; no mutations or guest diagnostics",
+ "twelve captured and inspected light/dark Overview/History images, including 390px and 320px"
+ ],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ },
+ {
+ "width": 320,
+ "height": 844
+ }
+ ],
+ "verified_at": "2026-10-04T19:16:25Z",
+ "browsers": [
+ "141.0.7390.37",
+ "26.0"
+ ],
+ "playwright_version": "1.56.1",
+ "proof_script": "frontend-modern/browser-tests/guest-memory-provenance.cjs",
+ "proof_cases": 24,
+ "limits": "Production drawer/history/model with real raw, API, unified API and canonical full/delta/fast consumers. Synthetic source payloads, History replies and disconnected websocket metadata. No full App, native QGA/thaw, filesystem writes, physical phone, reporter, installed or publication acceptance."
+}
diff --git a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx
index e7098f7af4..6834affd70 100644
--- a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx
+++ b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx
@@ -7,6 +7,7 @@ import { notificationStore } from '@/stores/notifications';
import { triggerPatrolRun } from '@/api/patrol';
import { t } from '@/i18n';
import type { Alert } from '@/types/api';
+import { isPulseSystemAlert } from '@/utils/alertScope';
import { useUpgradeNavigation } from '@/components/shared/useUpgradeNavigation';
import { getUpgradeActionDestination } from '@/stores/licenseCommercial';
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
@@ -54,7 +55,10 @@ export function InvestigateAlertButton(props: InvestigateAlertButtonProps) {
}
const showPatrolOption = () =>
- props.patrolOption === true && !isLocked() && !!(props.alert.resourceId || '').trim();
+ props.patrolOption === true &&
+ !isLocked() &&
+ !isPulseSystemAlert(props.alert) &&
+ !!(props.alert.resourceId || '').trim();
const usesPatrolPrimary = () => showPatrolOption() && props.variant !== 'icon';
const buttonTitle = () =>
isLocked()
@@ -97,7 +101,7 @@ export function InvestigateAlertButton(props: InvestigateAlertButtonProps) {
const startPatrolInvestigation = async () => {
const resourceId = (props.alert.resourceId || '').trim();
- if (!resourceId) {
+ if (isPulseSystemAlert(props.alert) || !resourceId) {
notificationStore.warning(t('alerts.assistant.patrol.noResource'));
return;
}
diff --git a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx
index a7194f6f5b..bc93b32485 100644
--- a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx
+++ b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n';
import type { Alert } from '@/types/api';
+import { makeSystemAlert } from '@/features/alerts/__fixtures__/systemAlerts';
import { getPublicPricingUrl } from '@/utils/pricingHandoff';
// ---------------------------------------------------------------------------
@@ -155,6 +156,44 @@ beforeEach(() => {
notificationStoreMock.info.mockReset();
});
+describe('system-alert investigation', () => {
+ it('retains resource Patrol for a monitored VM named Pulse', async () => {
+ const alert = makeAlert({ resourceName: 'Pulse' });
+ render(() => );
+ fireEvent.click(screen.getByRole('button', { name: 'Have Patrol investigate' }));
+ await waitFor(() => expect(triggerPatrolRunMock).toHaveBeenCalledTimes(1));
+ expect(triggerPatrolRunMock).toHaveBeenCalledWith({
+ resource_ids: ['vm-101'],
+ alert_identifier: alert.id,
+ alert_type: 'cpu',
+ });
+ expect(explainMock).not.toHaveBeenCalled();
+ });
+
+ it('retains metadata-only system scope on a historical ID', () => {
+ const alert = makeSystemAlert('future-condition', { id: 'legacy-id', resourceId: 'vm-wrong' });
+ render(() => );
+ fireEvent.click(screen.getByRole('button', { name: 'Ask Pulse Assistant about this alert' }));
+ expect(triggerPatrolRunMock).not.toHaveBeenCalled();
+ expect(openedContext().targetId).toBeUndefined();
+ expect(openedContext().handoffResources).toEqual([]);
+ });
+
+ it.each([undefined, { systemAlert: true, resourceType: 'vm' }])(
+ 'opens explanation, never resource Patrol, for a system alert with metadata %j',
+ (metadata) => {
+ const alert = makeSystemAlert('backup-evaluation', { resourceId: 'vm-wrong', metadata });
+ render(() => );
+ fireEvent.click(screen.getByRole('button', { name: 'Ask Pulse Assistant about this alert' }));
+ expect(explainMock).toHaveBeenCalledTimes(1);
+ expect(triggerPatrolRunMock).not.toHaveBeenCalled();
+ expect(openedContext().targetId).toBeUndefined();
+ expect(openedContext().handoffResources).toEqual([]);
+ expect(formatAlertValueMock).not.toHaveBeenCalled();
+ },
+ );
+});
+
// ---------------------------------------------------------------------------
// Rendering / visibility
// ---------------------------------------------------------------------------
diff --git a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts
index 3d0e532922..9f23bdbe9a 100644
--- a/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts
+++ b/frontend-modern/src/components/Alerts/__tests__/alertAssistantHandoffModel.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { Alert } from '@/types/api';
import { buildAlertAssistantHandoff } from '../alertAssistantHandoffModel';
+import { makeSystemAlert, SYSTEM_ALERT_TYPES } from '@/features/alerts/__fixtures__/systemAlerts';
function makeAlert(overrides: Partial = {}): Alert {
return {
@@ -22,6 +23,39 @@ function makeAlert(overrides: Partial = {}): Alert {
}
describe('alertAssistantHandoffModel', () => {
+ it.each(SYSTEM_ALERT_TYPES)(
+ 'explains Pulse %s without fabricated metric or resource context',
+ (type) => {
+ const alert = makeSystemAlert(type);
+ const { context } = buildAlertAssistantHandoff({ alert, resourceType: 'vm', vmid: 101 });
+ expect(context.targetType).toBeUndefined();
+ expect(context.targetId).toBeUndefined();
+ expect(context.handoffResources).toEqual([]);
+ expect(context.autonomousMode).toBe(false);
+ expect(context.handoffContext).toContain('Scope: Pulse itself (not a monitored resource)');
+ expect(context.handoffContext).toContain(alert.message);
+ expect(context.handoffContext).toContain(`Alert Identifier: ${alert.id}`);
+ expect(context.handoffContext).toContain('Operator Boundary:');
+ expect(context.handoffContext).not.toMatch(/Current Value:|Threshold:|Resource ID:|Node:/);
+ expect(context.context?.guestName).toBeUndefined();
+ expect(context.context?.vmid).toBeUndefined();
+ expect(context.briefing?.detailLines?.join('\n')).not.toMatch(/Current value|threshold/);
+ },
+ );
+
+ it('retains system scope for future types and legacy IDs carrying system metadata', () => {
+ const { context } = buildAlertAssistantHandoff({
+ alert: makeSystemAlert('future-condition', {
+ id: 'legacy-system-id',
+ resourceId: 'vm-wrong',
+ node: 'wrong-node',
+ metadata: { systemAlert: true, resourceType: 'vm' },
+ }),
+ });
+ expect(context.targetId).toBeUndefined();
+ expect(context.handoffResources).toEqual([]);
+ expect(context.handoffContext).not.toMatch(/wrong-node|vm-wrong/);
+ });
it('builds a visible approval-required alert handoff without command payloads', () => {
const handoff = buildAlertAssistantHandoff({
alert: makeAlert(),
diff --git a/frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts b/frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts
index a006e379af..3a6a08b9c7 100644
--- a/frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts
+++ b/frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts
@@ -4,6 +4,7 @@ import { DEFAULT_LOCALE, t, type SupportedLocale } from '@/i18n';
import { getCanonicalAlertId } from '@/features/alerts/identity';
import { formatAlertValue } from '@/utils/alertFormatters';
import { isMetricAlertType } from '@/utils/alerts';
+import { isPulseSystemAlert } from '@/utils/alertScope';
import { resolveAlertTargetType } from '@/utils/alertTargetTypes';
interface BuildAlertAssistantHandoffInput {
@@ -32,23 +33,27 @@ export function buildAlertAssistantHandoff({
// threshold 0.0%" in operator-facing copy is misleading default-zero
// noise. Suppress those fields and rely on alert.type + alert.message
// to convey what's wrong.
- const hasMetricValues = isMetricAlertType(alert.type);
+ const systemScoped = isPulseSystemAlert(alert);
+ const hasMetricValues = !systemScoped && isMetricAlertType(alert.type);
const currentValue = hasMetricValues ? formatAlertValue(alert.value, alert.type) : '';
const thresholdValue = hasMetricValues ? formatAlertValue(alert.threshold, alert.type) : '';
- const nodeLabel = alert.node ? alert.nodeDisplayName || alert.node : '';
+ const nodeLabel = !systemScoped && alert.node ? alert.nodeDisplayName || alert.node : '';
const levelLabel = formatAlertLevel(alert.level);
const modelLevelLabel = formatAlertLevel(alert.level, DEFAULT_LOCALE);
- const targetType = resolveAlertTargetType({
- alertType: alert.type,
- resourceType,
- metadataResourceType:
- typeof alert.metadata?.resourceType === 'string'
- ? (alert.metadata.resourceType as string)
- : undefined,
- resourceId: alert.resourceId,
- });
+ const targetType = systemScoped
+ ? undefined
+ : resolveAlertTargetType({
+ alertType: alert.type,
+ resourceType,
+ metadataResourceType:
+ typeof alert.metadata?.resourceType === 'string'
+ ? (alert.metadata.resourceType as string)
+ : undefined,
+ resourceId: alert.resourceId,
+ });
const handoffContext = buildAlertAssistantModelContext({
alert,
+ systemScoped,
alertIdentifier,
currentValue,
thresholdValue,
@@ -60,17 +65,19 @@ export function buildAlertAssistantHandoff({
return {
context: {
targetType,
- targetId: alert.resourceId,
+ targetId: systemScoped ? undefined : alert.resourceId,
autonomousMode: false,
handoffContext,
- handoffResources: [
- {
- id: alert.resourceId,
- name: alert.resourceName,
- type: targetType,
- node: alert.node,
- },
- ],
+ handoffResources: systemScoped
+ ? []
+ : [
+ {
+ id: alert.resourceId,
+ name: alert.resourceName,
+ type: targetType,
+ node: alert.node,
+ },
+ ],
briefing: {
sourceLabel: t('alerts.assistant.sourceLabel'),
title: t('alerts.assistant.title'),
@@ -103,9 +110,9 @@ export function buildAlertAssistantHandoff({
alertType: alert.type,
alertLevel: alert.level,
alertMessage: alert.message,
- guestName: alert.resourceName,
- node: alert.node,
- vmid,
+ guestName: systemScoped ? undefined : alert.resourceName,
+ node: systemScoped ? undefined : alert.node,
+ vmid: systemScoped ? undefined : vmid,
},
},
};
@@ -113,6 +120,7 @@ export function buildAlertAssistantHandoff({
function buildAlertAssistantModelContext({
alert,
+ systemScoped,
alertIdentifier,
currentValue,
thresholdValue,
@@ -121,6 +129,7 @@ function buildAlertAssistantModelContext({
levelLabel,
}: {
alert: Alert;
+ systemScoped: boolean;
alertIdentifier: string;
currentValue: string;
thresholdValue: string;
@@ -135,8 +144,9 @@ function buildAlertAssistantModelContext({
formatContextLine('Alert Type', alert.type),
formatContextLine('Alert Level', levelLabel),
formatContextLine('Alert Status', 'active'),
- formatContextLine('Resource', alert.resourceName),
- formatContextLine('Resource ID', alert.resourceId),
+ systemScoped ? 'Scope: Pulse itself (not a monitored resource)' : undefined,
+ formatContextLine(systemScoped ? 'Service' : 'Resource', alert.resourceName),
+ systemScoped ? undefined : formatContextLine('Resource ID', alert.resourceId),
formatContextLine('Current Value', currentValue),
formatContextLine('Threshold', thresholdValue),
formatContextLine('Duration', durationText),
diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx
index edbbc40bed..d7d9b65dfe 100644
--- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx
+++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx
@@ -309,14 +309,22 @@ describe('GuestDrawer', () => {
);
expect(history.querySelector('[data-history-current="disk"]')).toBeNull();
setGuest({ ...guest(), diskStatusReason: '', disk: { total: 100, used: 75, usage: 75 } });
- expect(history.querySelector('[data-history-last-known]')).toBeNull();
- expect(history.querySelector('[data-history-deferred]')).toBeNull();
+ expect(history.querySelector('[data-history-last-known="disk"]')).toBeNull();
+ expect(history.querySelector('[data-history-deferred="disk"]')).toBeNull();
expect(history.querySelector('[data-history-current="disk"]')).toHaveTextContent(
'Disk75.0%current',
);
+ // Fresh disk evidence cannot qualify the independent legacy memory value.
+ expect(history.querySelector('[data-history-current="memory"]')).toBeNull();
+ expect(history.querySelector('[data-history-unknown="memory"]')).toHaveTextContent(
+ 'freshness unknown',
+ );
+ expect(history.querySelector('[data-history-deferred="memory"]')).toHaveTextContent(
+ 'Not a current measurement.',
+ );
expect(history.querySelector('path')).toBeNull();
- // Existing drawer target recomputation follows each guest snapshot.
- // This presentation repair does not change that request ownership.
+ // Same target/range snapshots retain the chart and original History read.
+ expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalledTimes(1);
for (const [request] of chartsApiMocks.getMetricsHistory.mock.calls) {
expect(request).toMatchObject({
resourceType: 'vm',
diff --git a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx
index c8881b9407..1f1ace5910 100644
--- a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx
+++ b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx
@@ -328,7 +328,8 @@ const GuestDrawerHistoryGroupChart: Component
deferred,
descriptionId: `${inspectionId}-deferred-${index}`,
isCurrent: isFallback && !deferred,
- isLastKnown: isFallback && Boolean(deferred),
+ isLastKnown: isFallback && Boolean(deferred) && !deferred?.valueLabel,
+ isUnknown: isFallback && deferred?.valueLabel === 'freshness unknown',
valueLabel: hovered
? getGuestDrawerHistoryValueLabel([hovered.point], item.unit)
: activeTimestamp() !== null
@@ -393,6 +394,7 @@ const GuestDrawerHistoryGroupChart: Component
class="inline-flex items-center gap-1"
data-history-current={item.isCurrent ? item.metric : undefined}
data-history-last-known={item.isLastKnown ? item.metric : undefined}
+ data-history-unknown={item.isUnknown ? item.metric : undefined}
aria-describedby={item.deferred ? item.descriptionId : undefined}
>