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
This commit is contained in:
pulse-triage[bot]
2026-10-12 08:59:16 +01:00
parent f99c6f9e5c
commit 287ed04327
10 changed files with 1031 additions and 2 deletions
@@ -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.
@@ -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.
@@ -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;
});
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Linked guest filesystem History verification</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/linked-guest-history.tsx"></script>
</body>
</html>
@@ -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<Resource>(() => ({
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 (
<main class="min-h-screen bg-surface p-3 text-base-content">
<h1 class="text-base font-semibold">Linked guest History · {type}</h1>
<p class="my-2 text-xs text-muted">
Synthetic readings and controls. No guest command or backup runs.
</p>
<div class="mb-3 flex flex-wrap gap-2" aria-label="Fixture source controls">
<For
each={[
'Live agent',
'Silent agent',
'QGA recovered',
'Stopped guest',
'Fresh zero',
'Platform filesystems',
'Empty platform list',
'Stopped platform',
'Missing aggregate',
]}
>
{(value) => (
<button
class="min-h-11 rounded-sm border border-border px-3 text-xs"
onClick={() => setMode(value)}
>
{value}
</button>
)}
</For>
<button
class="min-h-11 rounded-sm border border-border px-3 text-xs"
onClick={() => setSeen(seen() + 3600000)}
>
Advance snapshot
</button>
</div>
<section class="mx-auto max-w-5xl" aria-label="Production drawer">
<Show
when={!search.has('guest')}
fallback={
<Show when={mapped.workloads()[0]}>
{(guest) => <GuestDrawer guest={guest()} onClose={() => {}} />}
</Show>
}
>
<ResourceDetailDrawer resource={resource()} presentation="table-row" />
</Show>
</section>
</main>
);
}
render(
() => (
<Router>
<Route
path="*"
component={() => (
<DarkModeContext.Provider
value={() => document.documentElement.classList.contains('dark')}
>
<WebSocketContext.Provider
value={
{
activeAlerts: {},
state: { pmg: [] },
connected: () => true,
} as unknown as WebSocketStore
}
>
<Fixture />
</WebSocketContext.Provider>
</DarkModeContext.Provider>
)}
/>
</Router>
),
document.getElementById('root')!,
);
@@ -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."
}
@@ -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(() => (
<ResourceDetailDrawer resource={resource()} presentation="table-row" />
));
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');
@@ -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,
@@ -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> = {}): 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();
});
});
@@ -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',
);
};