From 287ed04327b361845cbbe819f9c19ae4e6918f50 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Mon, 12 Oct 2026 08:59:16 +0100
Subject: [PATCH] Keep canonical guest History on its filesystem producer
Use the existing Workloads fallback and reporting-lease rules for linked Pulse-agent filesystem aggregates on Proxmox VMs and containers. Preserve native stopped withdrawal, platform-list precedence, measured zero and stored observations.
Add live/silent producer, power and mounted-drawer controls, plus own-parent desktop and phone browser evidence. This is presentation maintenance, not collection or native recovery.
Change-source: pulse-maintainer
---
.../HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md | 20 ++
.../subsystems/frontend-primitives.md | 25 ++
.../browser-tests/linked-guest-history.cjs | 320 ++++++++++++++++++
.../browser-tests/linked-guest-history.html | 12 +
.../browser-tests/linked-guest-history.tsx | 146 ++++++++
.../f14fcf66d296e5f7.json | 240 +++++++++++++
.../ResourceDetailDrawer.history.test.tsx | 98 ++++++
...rMetricsHistoryModel.branchcov0712.test.ts | 16 +
...icsHistoryModel.linked-filesystems.test.ts | 147 ++++++++
...resourceDetailDrawerMetricsHistoryModel.ts | 9 +-
10 files changed, 1031 insertions(+), 2 deletions(-)
create mode 100644 frontend-modern/browser-tests/linked-guest-history.cjs
create mode 100644 frontend-modern/browser-tests/linked-guest-history.html
create mode 100644 frontend-modern/browser-tests/linked-guest-history.tsx
create mode 100644 frontend-modern/browser-verification/f14fcf66d296e5f7.json
create mode 100644 frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts
diff --git a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md
index 6bd98740cc..ee19fa94e5 100644
--- a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md
+++ b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md
@@ -38,6 +38,26 @@ covered filesystem, guest liveness and only-prior-active restoration belong to
the native safety owners. These presentation checks do not set a release gate,
prove a safe guest or authorise a diagnostic/restart.
+## Canonical guest History filesystem ownership
+
+Automated: `resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts`,
+`ResourceDetailDrawer.history.test.tsx`, the existing workload mapper and
+source-admission controls. Use
+`frontend-modern/browser-tests/linked-guest-history.cjs` to inspect the real
+canonical and Workloads drawers on desktop Chromium and phone WebKit.
+A live linked agent remains current despite a QEMU-only read failure; its
+silent report is explicitly last known even while the platform stays online.
+Check provider-list precedence (including an empty list), source transitions
+on the same mounted drawer, native stopped withdrawal and fresh zero.
+LXC platform storage remains usable independently of in-guest agent state.
+Load stored observations, then change agent freshness and native power without
+removing, relabelling as current or fabricating chart samples.
+
+The fixture's controls and readings are synthetic, not installed guest
+recovery. Native thaw, writes to every covered filesystem, liveness, restart
+and prior-active restoration remain with the native owners; no release gate
+or safe-to-probe verdict follows from presentation proof.
+
## How To Use This Matrix
1. Run the automated proof first.
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 14eb3d24d3..fd47c81606 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -9403,3 +9403,28 @@ Verification connects `useThresholdsGuestData.test.ts`, both layouts in
`ResourceTable.test.tsx`, and the production hook/table/editor browser fixture
`guest-filesystem-thresholds.cjs`. Presentation proof is not native guest
recovery, thaw/writes/liveness, alert delivery or installed/released acceptance.
+
+### Canonical History follows the filesystem producer (12 October 2026)
+
+On Proxmox VMs and system containers, canonical History uses the same
+filesystem-ownership rule as Workloads: Proxmox's filesystem list takes
+precedence (including an explicit empty list); the linked Pulse agent owns
+the fallback only when that list is absent and its own list is present.
+A current linked-agent reading is not deferred by an unrelated QEMU read
+failure, and a silent linked agent's retained aggregate is never current
+just because the platform keeps the resource online. Native stopped state
+still withdraws in-guest carriers from either producer; stopped LXC platform
+storage and unrelated platform metrics remain independent.
+
+The existing aggregate is qualified, not recalculated from filesystem rows.
+Missing aggregates remain absent, measured zero remains usable, and neither
+Last seen nor a QEMU recovery renews a silent agent's report. Stored History,
+memory/CPU/network/IO, collection and guest commands are unchanged.
+
+Verification: `resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts`
+and mounted `ResourceDetailDrawer.history.test.tsx` connect live/silent agents,
+provider precedence, stopping/recovery and zero. The local
+`linked-guest-history.cjs` browser fixture uses the production canonical drawer,
+Workloads snapshot adapter/drawer, History and CSS on desktop and phone, with
+synthetic GET responses. These controls do not establish native restart, thaw,
+covered-filesystem writes, installed recovery or containing software.
diff --git a/frontend-modern/browser-tests/linked-guest-history.cjs b/frontend-modern/browser-tests/linked-guest-history.cjs
new file mode 100644
index 0000000000..d96fdda78c
--- /dev/null
+++ b/frontend-modern/browser-tests/linked-guest-history.cjs
@@ -0,0 +1,320 @@
+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/linked-guest-history/browser-final';
+const origin = 'http://127.0.0.1:5345';
+const runtime = [
+ 'frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts',
+ 'frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx',
+ 'frontend-modern/src/components/Workloads/GuestDrawer.tsx',
+ 'frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx',
+ 'frontend-modern/src/hooks/useWorkloads.ts',
+ 'frontend-modern/src/utils/workloadGuestPresentation.ts',
+];
+const fixture = ['cjs', 'tsx', 'html'].map(
+ (ext) => `frontend-modern/browser-tests/linked-guest-history.${ext}`,
+);
+const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
+(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)])),
+ fixture_hashes: Object.fromEntries(fixture.map((p) => [p, hash('/workspace/' + p)])),
+ cases: [],
+ screenshots: [],
+ cleanup: { browsers: false, server: false },
+ limits:
+ 'Production canonical drawer and Workloads snapshot adapter/drawer, History, clients and CSS with synthetic snapshots/GET responses. No native filesystem, thaw, restart, guest action, installed or release 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',
+ optimizeDeps: { entries: ['browser-tests/linked-guest-history.html'] },
+ server: { host: '127.0.0.1', port: 5345, strictPort: true },
+ });
+ let browser;
+ try {
+ await server.listen();
+ for (const [engineName, engine, width, height, dark] of [
+ ['desktop-light', chromium, 1280, 900, false],
+ ['phone-dark', webkit, 390, 844, true],
+ ]) {
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ for (const lxc of [false, true])
+ for (const guest of [false, true]) {
+ const name = `${guest ? 'workloads' : 'canonical'}-${lxc ? 'lxc' : 'vm'}-${engineName}`;
+ const page = await browser.newPage({
+ viewport: { width, height },
+ isMobile: width < 768,
+ hasTouch: width < 768,
+ locale: 'en-GB',
+ timezoneId: 'UTC',
+ });
+ page.setDefaultTimeout(12000);
+ page.setDefaultNavigationTimeout(45000);
+ const record = {
+ name,
+ width,
+ height,
+ dark,
+ lxc,
+ guest,
+ browser: browser.version(),
+ checks: [],
+ errors: [],
+ requests: [],
+ };
+ result.cases.push(record);
+ let stored = false;
+ page.on('pageerror', (e) => record.errors.push(e.message));
+ await page.route('**/*', (route) => {
+ const request = route.request(),
+ url = new URL(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();
+ assert.equal(request.method(), 'GET');
+ record.requests.push({ path: url.pathname, method: request.method(), stored });
+ if (url.pathname === '/api/metrics-store/history') {
+ assert.equal(url.searchParams.get('resourceType'), lxc ? 'system-container' : 'vm');
+ assert.equal(url.searchParams.get('resourceId'), 'site-a:pve1:105');
+ const start = Date.parse('2026-10-12T00:00:00Z'),
+ end = Date.parse('2026-10-12T01:00:00Z');
+ return route.fulfill({
+ json: {
+ resourceType: lxc ? 'system-container' : 'vm',
+ resourceId: 'site-a:pve1:105',
+ range: url.searchParams.get('range'),
+ start,
+ end,
+ source: 'store',
+ metrics: stored
+ ? {
+ disk: [
+ { timestamp: start, value: 12, min: 12, max: 12 },
+ { timestamp: end, value: 30, min: 30, max: 30 },
+ ],
+ }
+ : {},
+ },
+ });
+ }
+ return route.fulfill({
+ json:
+ url.pathname === '/api/license/runtime-capabilities'
+ ? {
+ capabilities: [],
+ limits: [],
+ max_history_days: 90,
+ hosted_mode: false,
+ runtime: { build: 'community' },
+ blocked_capabilities: [],
+ }
+ : {
+ success: true,
+ data: [],
+ alerts: [],
+ anomalies: [],
+ config: null,
+ capabilities: [],
+ relationships: [],
+ recentChanges: [],
+ available: false,
+ audits: [],
+ count: 0,
+ },
+ });
+ });
+ await page.addInitScript((isDark) => {
+ document.addEventListener('DOMContentLoaded', () =>
+ document.documentElement.classList.toggle('dark', isDark),
+ );
+ }, dark);
+ const suffix =
+ '?' +
+ new URLSearchParams({
+ ...(guest ? { guest: '1' } : {}),
+ ...(lxc ? { lxc: '1' } : {}),
+ }).toString();
+ await page.goto(origin + '/browser-tests/linked-guest-history.html' + suffix);
+ const activate = async (locator) => {
+ if (width < 768) await locator.tap();
+ else {
+ await locator.focus();
+ await locator.press('Enter');
+ }
+ };
+ await activate(page.getByRole('tab', { name: 'History', exact: true }));
+ const chart = page.locator('[data-history-group="utilization"]');
+ const current = () => chart.locator('[data-history-current="disk"]');
+ const prior = () => chart.locator('[data-history-last-known="disk"]');
+ const deferred = () => chart.locator('[data-history-deferred="disk"]');
+ const set = (label) => activate(page.getByRole('button', { name: label, exact: true }));
+ const check = async (label, fn) => {
+ await fn();
+ record.checks.push(label);
+ };
+ const text = async (locator, expression) => {
+ await locator.waitFor();
+ assert.match(await locator.innerText(), expression);
+ };
+ const missing = async (locator) => assert.equal(await locator.count(), 0);
+ const capture = async (state) => {
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
+ });
+ const file = `${name}-${state}.png`;
+ await page.screenshot({ path: path.join(output, file), fullPage: true });
+ result.screenshots.push({ file, name, state, sha256: hash(path.join(output, file)) });
+ const geometry = await page.evaluate(() => ({
+ width: document.documentElement.clientWidth,
+ scroll: document.documentElement.scrollWidth,
+ }));
+ assert.ok(geometry.scroll <= geometry.width + 1, JSON.stringify(geometry));
+ };
+ await check('Live linked reading survives QGA failure', async () => {
+ await text(current(), /62.0%\s*current/);
+ await missing(deferred());
+ });
+ await page.evaluate(() => {
+ window.__chart = document.querySelector('[data-history-group="utilization"]');
+ });
+ await check('Silent agent is last known despite online platform', async () => {
+ await set('Silent agent');
+ await text(prior(), /62.0%\s*last known/);
+ await missing(current());
+ await text(deferred(), /The Pulse Agent in this guest stopped reporting/);
+ });
+ await capture('silent');
+ await check('QGA recovery and Last seen cannot renew the agent report', async () => {
+ await set('QGA recovered');
+ await set('Advance snapshot');
+ await text(prior(), /62.0%\s*last known/);
+ await text(deferred(), /Pulse Agent/);
+ });
+ await check('Stopped in-guest reading is unavailable, not last known', async () => {
+ await set('Stopped guest');
+ await missing(current());
+ await missing(prior());
+ await text(deferred(), /unavailable/);
+ });
+ await capture('stopped');
+ await check('Fresh linked-agent zero is current', async () => {
+ await set('Fresh zero');
+ await text(current(), /0.0%\s*current/);
+ await missing(deferred());
+ });
+ await check('Missing aggregate is not fabricated from filesystem rows', async () => {
+ await set('Missing aggregate');
+ await missing(current());
+ await missing(prior());
+ });
+ await check('Platform filesystem precedence uses its own read state', async () => {
+ await set('Platform filesystems');
+ if (lxc) {
+ await text(current(), /62.0%\s*current/);
+ await missing(deferred());
+ } else {
+ await text(prior(), /62.0%\s*last known/);
+ await text(deferred(), /operation lock/);
+ }
+ });
+ await check('Explicit empty platform list does not borrow agent freshness', async () => {
+ await set('Empty platform list');
+ if (lxc) {
+ await text(current(), /62.0%\s*current/);
+ } else {
+ await text(prior(), /62.0%\s*last known/);
+ await text(deferred(), /operation lock/);
+ }
+ });
+ await check('Stopped LXC platform storage stays independent', async () => {
+ await set('Stopped platform');
+ if (lxc) {
+ await text(current(), /62.0%\s*current/);
+ await missing(deferred());
+ } else {
+ await missing(current());
+ await missing(prior());
+ await text(deferred(), /VM is stopped/);
+ }
+ });
+ await check(
+ 'Stored observations remain intact through source/power changes',
+ async () => {
+ await set('Live agent');
+ stored = true;
+ await activate(page.getByRole('button', { name: 'Refresh history', exact: true }));
+ await chart.locator('path').waitFor();
+ const before = await chart.locator('path').getAttribute('d');
+ assert.ok(before);
+ await set('Silent agent');
+ await text(deferred(), /Pulse Agent/);
+ assert.equal(await chart.locator('path').getAttribute('d'), before);
+ await missing(prior());
+ await set('Stopped guest');
+ await text(deferred(), /unavailable/);
+ assert.equal(await chart.locator('path').getAttribute('d'), before);
+ await missing(current());
+ assert.match(await chart.innerText(), /30.0%/);
+ },
+ );
+ await check(
+ 'Same mounted chart, usable independent metrics and no fabricated samples',
+ async () => {
+ assert.equal(
+ await page.evaluate(
+ () =>
+ window.__chart === document.querySelector('[data-history-group="utilization"]'),
+ ),
+ true,
+ );
+ await text(chart.locator('[data-history-current="cpu"]'), /10.0%\s*current/);
+ await text(chart.locator('[data-history-current="memory"]'), /0.0%\s*current/);
+ assert.equal(await chart.locator('path').count(), 1);
+ assert.deepEqual(record.errors, []);
+ assert.ok(record.requests.every((r) => !r.blocked));
+ },
+ );
+ await capture('stored-stopped');
+ await page.close();
+ }
+ await browser.close();
+ browser = null;
+ }
+ result.result = 'passed';
+ } catch (error) {
+ result.error = String(error);
+ throw error;
+ } finally {
+ if (browser) await browser.close();
+ result.cleanup.browsers = true;
+ await server.close();
+ result.cleanup.server = true;
+ fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2) + '\n');
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/linked-guest-history.html b/frontend-modern/browser-tests/linked-guest-history.html
new file mode 100644
index 0000000000..47774357a3
--- /dev/null
+++ b/frontend-modern/browser-tests/linked-guest-history.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Linked guest filesystem History verification
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/linked-guest-history.tsx b/frontend-modern/browser-tests/linked-guest-history.tsx
new file mode 100644
index 0000000000..dd634487d8
--- /dev/null
+++ b/frontend-modern/browser-tests/linked-guest-history.tsx
@@ -0,0 +1,146 @@
+// Only snapshots and the fixture controls are synthetic. Source mapping,
+// drawers, History, clients and CSS are production code; no guest action.
+import { createMemo, createSignal, For, Show } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
+import { ResourceDetailDrawer } from '../src/components/Infrastructure/ResourceDetailDrawer';
+import { GuestDrawer } from '../src/components/Workloads/GuestDrawer';
+import { useWorkloads } from '../src/hooks/useWorkloads';
+import { DarkModeContext, WebSocketContext, type WebSocketStore } from '../src/contexts/appRuntime';
+import { syncAIRuntimeSettings } from '../src/stores/aiRuntimeState';
+import type { Resource } from '../src/types/resource';
+import '../src/index.css';
+syncAIRuntimeSettings({ discovery_enabled: false });
+
+function Fixture() {
+ const search = new URLSearchParams(location.search);
+ const type = search.has('lxc') ? 'system-container' : 'vm';
+ const [mode, setMode] = createSignal('Live agent');
+ const [seen, setSeen] = createSignal(Date.parse('2026-10-12T01:00:00Z'));
+ const disk = () => ({
+ mountpoint: '/',
+ total: 100,
+ used: mode() === 'Fresh zero' ? 0 : 62,
+ free: 38,
+ });
+ const stopped = () => ['Stopped guest', 'Stopped platform'].includes(mode());
+ const platform = () =>
+ ['Platform filesystems', 'Empty platform list', 'Stopped platform'].includes(mode());
+ const silent = () => ['Silent agent', 'QGA recovered'].includes(mode());
+ const resource = createMemo(() => ({
+ id: 'site-a:pve1:105',
+ type,
+ name: 'Linked guest',
+ displayName: 'Linked guest',
+ platformId: 'site-a',
+ platformType: 'proxmox-pve',
+ sourceType: 'hybrid',
+ status: 'online',
+ lastSeen: seen(),
+ metricsTarget: { resourceType: type, resourceId: 'site-a:pve1:105' },
+ cpu: { current: 10 },
+ memory: {
+ current: 0,
+ total: 100,
+ used: 0,
+ observation: { state: 'current', source: 'agent', observedAt: '2026-10-12T00:00:00Z' },
+ },
+ disk:
+ mode() === 'Missing aggregate'
+ ? undefined
+ : { current: disk().used, total: 100, used: disk().used },
+ network: { rxBytes: 0, txBytes: 200 },
+ proxmox: {
+ nodeName: 'pve1',
+ vmid: 105,
+ instance: 'site-a',
+ runtimeStatus: stopped() ? 'stopped' : 'running',
+ diskStatusReason:
+ mode() === 'QGA recovered' || stopped()
+ ? ''
+ : platform()
+ ? 'prev-vm-locked'
+ : 'agent-not-running',
+ disks: platform() ? (mode() === 'Empty platform list' ? [] : [disk()]) : undefined,
+ },
+ agent: { agentId: 'in-guest', stale: silent(), disks: [disk()] },
+ }));
+ const mapped = useWorkloads(() => true, { resourceSnapshot: () => [resource()] });
+ return (
+
+ Linked guest History ยท {type}
+
+ Synthetic readings and controls. No guest command or backup runs.
+
+
+
+ {(value) => (
+
+ )}
+
+
+
+
+
+ {(guest) => {}} />}
+
+ }
+ >
+
+
+
+
+ );
+}
+render(
+ () => (
+
+ (
+ document.documentElement.classList.contains('dark')}
+ >
+ true,
+ } as unknown as WebSocketStore
+ }
+ >
+
+
+
+ )}
+ />
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification/f14fcf66d296e5f7.json b/frontend-modern/browser-verification/f14fcf66d296e5f7.json
new file mode 100644
index 0000000000..7367f6d339
--- /dev/null
+++ b/frontend-modern/browser-verification/f14fcf66d296e5f7.json
@@ -0,0 +1,240 @@
+{
+ "version": 1,
+ "base_sha": "f99c6f9e5c3aba2ad025bc24da7c6bd6c7e5a85a",
+ "verified_at": "2026-10-12T07:58:01.128408Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts": "257ccee974aeab51c6d35360117986e9f4aed92891cb2a153b656e57e71a584f"
+ },
+ "routes": [
+ "Existing canonical ResourceDetailDrawer / History",
+ "Existing Workloads snapshot adapter -> GuestDrawer / History (production components in local fixture)"
+ ],
+ "states": [
+ "Current linked-agent VM disk aggregate survives unrelated QGA read failures",
+ "Silent linked-agent VM/LXC aggregate is last known despite online platform; QGA recovery/Last seen do not renew it",
+ "Stopped in-guest VM/LXC reading is unavailable, no last-known fallback; native stopped still wins",
+ "Platform filesystem lists retain source precedence, including explicit empty lists; LXC platform storage stays independent",
+ "Measured zero remains current, missing aggregate is not fabricated; stored paths and observations survive source/power updates",
+ "Independent CPU/memory/network readings and same-mounted chart remain intact on desktop and phone"
+ ],
+ "interactions": [
+ "History and synthetic source controls activated with keyboard focus/Enter on desktop or touch on phone",
+ "Same-ID source, reporting lease, power and aggregate snapshots update without remount",
+ "Refresh history reads two actual synthetic stored points through production client; later source/power updates keep their path unchanged"
+ ],
+ "viewports": [
+ {
+ "name": "desktop-light",
+ "width": 1280,
+ "height": 900,
+ "browser": "Chromium 141.0.7390.37"
+ },
+ {
+ "name": "phone-dark",
+ "width": 390,
+ "height": 844,
+ "browser": "WebKit 26.0"
+ }
+ ],
+ "evidence": {
+ "command": "pulse-worker-browser frontend-modern/browser-tests/linked-guest-history.cjs",
+ "playwright": "1.56.1",
+ "integration_lock_version": "1.56.1",
+ "completed_checks": 88,
+ "cases": 8,
+ "fixture_sha256": {
+ "frontend-modern/browser-tests/linked-guest-history.cjs": "1767ebebd11d4db018b1f3bea7a89ad9fc7692e8cb084bd7c9d89fe87ef4ac0f",
+ "frontend-modern/browser-tests/linked-guest-history.tsx": "feb16a0b04a82ae722b53f9e71969be89e70344188c5f2b0deb2853a5f96309f",
+ "frontend-modern/browser-tests/linked-guest-history.html": "2a1bd63a32bc4be49d7ae9f0dc9c91932f11b3e4c3f0c8f38c7110fd0095ccae"
+ },
+ "runtime_sha256": {
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts": "257ccee974aeab51c6d35360117986e9f4aed92891cb2a153b656e57e71a584f",
+ "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "9e7ee3e3cbb5f8da38cf43e0aaecd105f43333d16df7338e849d3ccaf6804d8f",
+ "frontend-modern/src/components/Workloads/GuestDrawer.tsx": "604deee9674aa3d97e45ccf7bc5b8c5dda27017dee2f235c107750911d2981b7",
+ "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "8b94c3d9409f46bedf812b00ff9f3156da9ace6885ec04456ea5fed59d842127",
+ "frontend-modern/src/hooks/useWorkloads.ts": "1439f5bc51ad5e362eb21b65b9d95b870f84cdae823770f4ae41dd848905703d",
+ "frontend-modern/src/utils/workloadGuestPresentation.ts": "71a2d2e62411c70c0eb609e68cd4963dac483be59a5be6afcf96c1ce2191436f"
+ },
+ "captures": [
+ {
+ "file": "canonical-vm-desktop-light-silent.png",
+ "name": "canonical-vm-desktop-light",
+ "state": "silent",
+ "sha256": "1b9c34d68c8cf24be7114c06d9976188f5bd9419af9e5b461d8a8ba484d50b4f",
+ "viewed": true
+ },
+ {
+ "file": "canonical-vm-desktop-light-stopped.png",
+ "name": "canonical-vm-desktop-light",
+ "state": "stopped",
+ "sha256": "45fd35767c8246a298cd18957cdacf88396bce6c41327a6494bb94cd15567d7f",
+ "viewed": true
+ },
+ {
+ "file": "canonical-vm-desktop-light-stored-stopped.png",
+ "name": "canonical-vm-desktop-light",
+ "state": "stored-stopped",
+ "sha256": "c15788cb4d7f45b85c4644f6dd14578e863b0f1d7ebf507bd82e23ea3683a254",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-desktop-light-silent.png",
+ "name": "workloads-vm-desktop-light",
+ "state": "silent",
+ "sha256": "dea3ac6a77870247ae166a2f60ce3da4266b6e70f40f8b23dbef08d06d0bf2ad",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-desktop-light-stopped.png",
+ "name": "workloads-vm-desktop-light",
+ "state": "stopped",
+ "sha256": "876e1b63a462e7ff6bf9321f15c025b8709db3f101312a3f4dc8127ec8928aec",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-desktop-light-stored-stopped.png",
+ "name": "workloads-vm-desktop-light",
+ "state": "stored-stopped",
+ "sha256": "02bad262d41990992bcf008dfb5dbe4fbd97a7247fd4bbce48c2740ab8713dcc",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-desktop-light-silent.png",
+ "name": "canonical-lxc-desktop-light",
+ "state": "silent",
+ "sha256": "4858ed050197194789d58205bfe4fa3ab54ac3bbdc7f6e041d00adb6ec2f6634",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-desktop-light-stopped.png",
+ "name": "canonical-lxc-desktop-light",
+ "state": "stopped",
+ "sha256": "58cfcedff92ace012f61a0989ab7a0caba0a760736968da22485a6912320013d",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-desktop-light-stored-stopped.png",
+ "name": "canonical-lxc-desktop-light",
+ "state": "stored-stopped",
+ "sha256": "7d8eb01333adafbf4ab43dd5e5097dd39a42369dd0b0de9e9d3dfab4761bacd1",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-desktop-light-silent.png",
+ "name": "workloads-lxc-desktop-light",
+ "state": "silent",
+ "sha256": "ed66a394887732f81eb3fd0cec0e7e4e1775ad79d6a9d080c8d7bec373760bcc",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-desktop-light-stopped.png",
+ "name": "workloads-lxc-desktop-light",
+ "state": "stopped",
+ "sha256": "7acc180629fe474edd6126d7bd9fa4d5d133de46aaebbee32b1272ab1e1556e0",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-desktop-light-stored-stopped.png",
+ "name": "workloads-lxc-desktop-light",
+ "state": "stored-stopped",
+ "sha256": "19250fac504ee0811b84caeeea4acdc3dd4ced30d794d34c2d9a3a4b135e9189",
+ "viewed": true
+ },
+ {
+ "file": "canonical-vm-phone-dark-silent.png",
+ "name": "canonical-vm-phone-dark",
+ "state": "silent",
+ "sha256": "c519511e99e6e24208e4f05a2da121756d76edeb514f209306faa4d5de5f9a3a",
+ "viewed": true
+ },
+ {
+ "file": "canonical-vm-phone-dark-stopped.png",
+ "name": "canonical-vm-phone-dark",
+ "state": "stopped",
+ "sha256": "1085f3fb0a99654fcc807b3a5fb492704f2a3e150f0d996d8407c0ccd9fb173b",
+ "viewed": true
+ },
+ {
+ "file": "canonical-vm-phone-dark-stored-stopped.png",
+ "name": "canonical-vm-phone-dark",
+ "state": "stored-stopped",
+ "sha256": "4eedf5fcc6fb4c54c5a4be70d4ac24d18230b15b11ee9af8c8bbdd42d956d16a",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-phone-dark-silent.png",
+ "name": "workloads-vm-phone-dark",
+ "state": "silent",
+ "sha256": "84561a90090f3d945b944c4298b9a2961550fe59e945922aa2f89951e690af79",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-phone-dark-stopped.png",
+ "name": "workloads-vm-phone-dark",
+ "state": "stopped",
+ "sha256": "188c4e63560a61da7636165903e06e6733615c22042340ef852303d3128c7f79",
+ "viewed": true
+ },
+ {
+ "file": "workloads-vm-phone-dark-stored-stopped.png",
+ "name": "workloads-vm-phone-dark",
+ "state": "stored-stopped",
+ "sha256": "4899356a909fff6ffd12e2e4f1ac301e1e0fefa0f2032d61ddfa22a88459ca4c",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-phone-dark-silent.png",
+ "name": "canonical-lxc-phone-dark",
+ "state": "silent",
+ "sha256": "1a3503060ef8060fe1165fccec8190abca8fe2c55c1866d4652c156c7d413402",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-phone-dark-stopped.png",
+ "name": "canonical-lxc-phone-dark",
+ "state": "stopped",
+ "sha256": "6a0e50472158269e635c37c4151b63ccfd8cd4b8b8e351c2ae62c8abf420fd4a",
+ "viewed": true
+ },
+ {
+ "file": "canonical-lxc-phone-dark-stored-stopped.png",
+ "name": "canonical-lxc-phone-dark",
+ "state": "stored-stopped",
+ "sha256": "9bc76bcad4019b98a739d10a3e1ab1cc38aeae3e6bb2de01faa7f623c2ca5c20",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-phone-dark-silent.png",
+ "name": "workloads-lxc-phone-dark",
+ "state": "silent",
+ "sha256": "d6a324d28b2e2af6175bd8f09c9b1b17949e03e0e5519e9c294c90395302e5fa",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-phone-dark-stopped.png",
+ "name": "workloads-lxc-phone-dark",
+ "state": "stopped",
+ "sha256": "ae4204af814c04e9e8ffb49895edfe592e2e628f8fa85924b86044af682e7193",
+ "viewed": true
+ },
+ {
+ "file": "workloads-lxc-phone-dark-stored-stopped.png",
+ "name": "workloads-lxc-phone-dark",
+ "state": "stored-stopped",
+ "sha256": "88225624fbc638302a2f210c9421e5a12df7f99fb561b7875911a5e4d9072e13",
+ "viewed": true
+ }
+ ],
+ "cleanup": {
+ "browsers": true,
+ "server": true
+ },
+ "result_sha256": "8c6dc1162c9191540362d1516bfc9eed33c2ef4aaec92f899c162b4b7ebd3516",
+ "prior_attempt": "First browser attempt had a missing synthetic WebSocketContext.Provider; zero checks, client1, captured page error, browsers/server stopped. Fixture-only provider correction; runtime/deadlines/thresholds unchanged."
+ },
+ "limits": "Production canonical drawer and Workloads snapshot adapter/drawer, History, clients and CSS with synthetic snapshots/GET responses. No native filesystem, thaw, restart, guest action, installed or release acceptance."
+}
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 30ea9130be..041894d3eb 100644
--- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx
+++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx
@@ -322,6 +322,104 @@ describe('ResourceDetailDrawer change history section', () => {
}
});
+ it.each(['vm', 'system-container'] as const)(
+ 'keeps the mounted %s History on the selected filesystem producer',
+ async (type) => {
+ resetCreateNonSuspendingQueryCacheForTest();
+ const disks = [{ mountpoint: '/', total: 100, used: 62, free: 38 }];
+ const source = baseResource({
+ id: 'linked-filesystem-guest',
+ type,
+ platformType: 'proxmox-pve',
+ status: 'online',
+ metricsTarget: { resourceType: type, resourceId: 'linked:pve:105' },
+ cpu: { current: 10 },
+ memory: {
+ current: 0,
+ observation: { state: 'current', source: 'agent', observedAt: '2026-10-12T00:00:00Z' },
+ },
+ disk: { current: 62 },
+ proxmox: { runtimeStatus: 'running', diskStatusReason: 'agent-not-running' },
+ agent: { agentId: 'in-guest', stale: false, disks },
+ });
+ const read = vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue({
+ resourceType: type,
+ resourceId: 'linked:pve:105',
+ range: '24h',
+ start: Date.parse('2026-10-12T00:00:00Z'),
+ end: Date.parse('2026-10-12T01:00:00Z'),
+ source: 'store',
+ metrics: {},
+ });
+ const [resource, setResource] = createSignal(source);
+ const view = render(() => (
+
+ ));
+ try {
+ fireEvent.click(screen.getByRole('tab', { name: 'History' }));
+ await waitFor(() => expect(read).toHaveBeenCalledTimes(1));
+ const chart = view.container.querySelector('[data-history-group="utilization"]')!;
+ expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent(
+ '62.0%current',
+ );
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toBeNull();
+ setResource({
+ ...source,
+ lastSeen: source.lastSeen + 3600000,
+ agent: { ...source.agent, stale: true },
+ });
+ expect(chart.querySelector('[data-history-current="disk"]')).toBeNull();
+ expect(chart.querySelector('[data-history-last-known="disk"]')).toHaveTextContent(
+ '62.0%last known',
+ );
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toHaveTextContent(
+ 'The Pulse Agent in this guest stopped reporting.',
+ );
+ setResource({
+ ...source,
+ proxmox: { runtimeStatus: 'running', disks, diskStatusReason: 'prev-vm-locked' },
+ });
+ if (type === 'vm') {
+ expect(chart.querySelector('[data-history-last-known="disk"]')).toHaveTextContent(
+ '62.0%last known',
+ );
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toHaveTextContent(
+ 'VM operation lock',
+ );
+ } else {
+ expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent(
+ '62.0%current',
+ );
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toBeNull();
+ }
+ setResource({ ...source, proxmox: { runtimeStatus: 'stopped' } });
+ expect(chart.querySelector('[data-history-current="disk"]')).toBeNull();
+ expect(chart.querySelector('[data-history-last-known="disk"]')).toBeNull();
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toHaveTextContent(
+ 'unavailable',
+ );
+ setResource({ ...source, disk: { current: 0 }, status: 'offline' });
+ expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent(
+ '0.0%current',
+ );
+ expect(chart.querySelector('[data-history-deferred="disk"]')).toBeNull();
+ expect(chart.querySelector('[data-history-current="memory"]')).toHaveTextContent(
+ '0.0%current',
+ );
+ expect(chart.querySelector('[data-history-current="cpu"]')).toHaveTextContent(
+ '10.0%current',
+ );
+ expect(view.container.querySelector('[data-history-group="utilization"]')).toBe(chart);
+ expect(chart.querySelector('path')).toBeNull();
+ expect(read).toHaveBeenCalledTimes(1);
+ } finally {
+ view.unmount();
+ read.mockRestore();
+ resetCreateNonSuspendingQueryCacheForTest();
+ }
+ },
+ );
+
it('keeps current readings separate from stored history samples', () => {
expect(guestDrawerHistorySource).toContain('currentMetrics');
expect(guestDrawerHistorySource).toContain('No stored history in this range');
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts
index 3ae5ee2af8..d90fe3717d 100644
--- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts
+++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts
@@ -95,6 +95,22 @@ describe('canonical drawer History honours selected guest observation provenance
},
);
+ it('retains a legitimate linked-agent filesystem zero without calling it current', () => {
+ const value = resource('current', 0);
+ value.disk = { current: 0, used: 0, total: 100 };
+ value.proxmox = { runtimeStatus: 'running', diskStatusReason: '' };
+ value.agent = { stale: true, disks: [{ mountpoint: '/', used: 0, total: 100, free: 100 }] };
+ expect(getResourceMetricsHistoryCurrentMetrics(value)).toMatchObject({
+ disk: undefined,
+ usage: undefined,
+ memory: 0,
+ });
+ expect(getResourceMetricsHistoryDeferredMetrics(value).disk).toEqual({
+ lastKnownValue: 0,
+ message: 'Using last known disk stats. The Pulse Agent in this guest stopped reporting.',
+ });
+ });
+
it('keeps unavailable, true zero and independent current memory distinct from disk deferral', () => {
expect(
getResourceMetricsHistoryDeferredMetrics(resource('unavailable', 0)).memory.lastKnownValue,
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts
new file mode 100644
index 0000000000..3da29e12c9
--- /dev/null
+++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.linked-filesystems.test.ts
@@ -0,0 +1,147 @@
+import { describe, expect, it } from 'vitest';
+import type { Resource } from '@/types/resource';
+import { guestDiskDeferrals } from '@/components/Workloads/__fixtures__/guestDiskDeferrals';
+import {
+ getResourceMetricsHistoryCurrentMetrics as current,
+ getResourceMetricsHistoryDeferredMetrics as deferred,
+} from '../resourceDetailDrawerMetricsHistoryModel';
+
+const agentDisk = { mountpoint: '/', total: 100, used: 62, free: 38 };
+const guest = (patch: Partial = {}): Resource => ({
+ id: 'site-a:pve1:105',
+ type: 'vm',
+ name: 'Linked guest',
+ displayName: 'Linked guest',
+ platformId: 'site-a',
+ platformType: 'proxmox-pve',
+ sourceType: 'hybrid',
+ status: 'online',
+ lastSeen: Date.parse('2026-10-12T01:00:00Z'),
+ cpu: { current: 10 },
+ memory: {
+ current: 0,
+ observation: { state: 'current', source: 'agent', observedAt: '2026-10-12T00:00:00Z' },
+ },
+ disk: { current: 62, total: 100, used: 62 },
+ network: { rxBytes: 0, txBytes: 200 },
+ proxmox: { runtimeStatus: 'running', diskStatusReason: 'agent-not-running' },
+ agent: { agentId: 'in-guest', stale: false, disks: [agentDisk] },
+ ...patch,
+});
+
+describe('canonical History filesystem source ownership', () => {
+ it.each([
+ 'agent-not-running',
+ 'agent-error',
+ 'permission-denied',
+ ...guestDiskDeferrals.map(([r]) => r),
+ ])('uses the live linked agent instead of the unrelated QGA %s read failure', (reason) => {
+ const value = guest({ proxmox: { runtimeStatus: 'running', diskStatusReason: reason } });
+ expect(current(value)).toMatchObject({ disk: 62, usage: 62, cpu: 10, memory: 0, netin: 0 });
+ expect(deferred(value).disk).toBeUndefined();
+ value.proxmox!.diskStatusReason = `prev-${reason}`;
+ expect(current(value).disk).toBe(62);
+ expect(deferred(value).disk).toBeUndefined();
+ });
+
+ it.each(['vm', 'system-container'] as const)(
+ 'qualifies a silent linked agent on an online %s',
+ (type) => {
+ const value = guest({ type, agent: { stale: true, disks: [agentDisk] } });
+ expect(current(value)).toMatchObject({
+ disk: undefined,
+ usage: undefined,
+ cpu: 10,
+ memory: 0,
+ });
+ expect(deferred(value).disk).toEqual({
+ lastKnownValue: 62,
+ message: 'Using last known disk stats. The Pulse Agent in this guest stopped reporting.',
+ });
+ value.lastSeen += 3600000;
+ value.proxmox!.diskStatusReason = '';
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk.lastKnownValue).toBe(62);
+ },
+ );
+
+ it.each(['stopped', ' STOPPED ', 'offline'])(
+ 'native %s wins over a live or silent linked agent',
+ (runtimeStatus) => {
+ for (const stale of [false, true]) {
+ const value = guest({ proxmox: { runtimeStatus }, agent: { stale, disks: [agentDisk] } });
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk).toEqual({
+ lastKnownValue: undefined,
+ message: 'Guest filesystem stats unavailable while the VM is stopped.',
+ });
+ }
+ },
+ );
+
+ it.each([{ disks: [] }, { disks: [agentDisk] }])(
+ 'does not borrow an agent when the platform owns disks (%j)',
+ ({ disks }) => {
+ const value = guest({
+ proxmox: { runtimeStatus: 'running', disks, diskStatusReason: 'prev-vm-locked' },
+ });
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk.message).toContain('VM operation lock');
+ value.agent!.stale = true;
+ value.proxmox!.diskStatusReason = '';
+ expect(current(value).disk).toBe(62);
+ expect(deferred(value).disk).toBeUndefined();
+ },
+ );
+
+ it('does not infer filesystem ownership from a mere agent identity', () => {
+ const value = guest({ agent: { agentId: 'in-guest', stale: false } });
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk.lastKnownValue).toBeUndefined();
+ });
+
+ it('preserves current linked-agent zero and does not fabricate an absent aggregate', () => {
+ const value = guest({ disk: { current: 0, used: 0, total: 100 } });
+ expect(current(value)).toMatchObject({ disk: 0, usage: 0 });
+ expect(deferred(value).disk).toBeUndefined();
+ value.disk = undefined;
+ expect(current(value).disk).toBeUndefined();
+ value.agent!.stale = true;
+ expect(deferred(value).disk.lastKnownValue).toBeUndefined();
+ });
+
+ it('retains LXC platform storage while stopped, but not its in-guest agent reading', () => {
+ const value = guest({
+ type: 'system-container',
+ proxmox: { runtimeStatus: 'stopped', disks: [agentDisk] },
+ });
+ expect(current(value)).toMatchObject({ disk: 62, usage: 62 });
+ expect(deferred(value).disk).toBeUndefined();
+ value.proxmox!.disks = undefined;
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk).toEqual({
+ lastKnownValue: undefined,
+ message: 'Filesystem usage is unavailable.',
+ });
+ });
+
+ it.each(['vm', 'system-container'] as const)(
+ 'keeps a linked-only %s in Proxmox scopes qualified',
+ (type) => {
+ const value = guest({
+ type,
+ platformType: 'agent',
+ platformScopes: ['agent', 'proxmox-pve'],
+ agent: { stale: true, disks: [agentDisk] },
+ });
+ expect(current(value).disk).toBeUndefined();
+ expect(deferred(value).disk.lastKnownValue).toBe(62);
+ },
+ );
+
+ it('does not impose QGA read metadata on a vSphere-only VM', () => {
+ const value = guest({ platformType: 'vmware-vsphere', platformScopes: ['vmware-vsphere'] });
+ expect(current(value)).toMatchObject({ disk: 62, usage: 62 });
+ expect(deferred(value).disk).toBeUndefined();
+ });
+});
diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts
index d0c8a50c8a..a8f78cb0a0 100644
--- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts
+++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts
@@ -174,7 +174,7 @@ const memoryReading = (resource: Resource) =>
);
const guestDiskReading = (resource: Resource) => {
- if (resource.type !== 'vm' || !isProxmoxGuest(resource)) return null;
+ if (!isProxmoxGuest(resource)) return null;
// A linked source can keep the canonical resource online while Proxmox
// reports the VM stopped. Judge filesystem carriers by the native state,
// using the same read admission as Workloads and Alerts, not Last seen.
@@ -185,8 +185,13 @@ const guestDiskReading = (resource: Resource) => {
{
status: status === 'offline' ? 'stopped' : status,
diskStatusReason: resource.proxmox?.diskStatusReason,
+ // Match the Workloads projection: the linked agent owns filesystem
+ // readings only when Proxmox supplied no filesystem list. Its own
+ // reporting lease then qualifies them, not the unrelated QGA failure.
+ disksFromAgent: resource.proxmox?.disks == null && resource.agent?.disks != null,
+ agentStale: resource.agent?.stale === true,
},
- true,
+ resource.type === 'vm',
);
};