Integrate reviewed truthful guest History presentation

Preserve the exact reviewed Web commits and their own-parent browser receipts alongside the supplied main frontier.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 07:20:38 +01:00
19 changed files with 1528 additions and 77 deletions
@@ -20,6 +20,29 @@
## Purpose
All drawer History fallbacks must distinguish current observations from retained, unavailable or freshness-unknown guest readings, using original memory evidence and filesystem read reasons.
### Canonical drawer History preserves guest read provenance
The shared resource drawer passes selected memory observation state/source/time
and Proxmox VM disk-status reasons into the existing History deferred-reading
path. Last-known values remain labelled evidence; unavailable numeric carriers
never become current values. Unannotated Proxmox VM/LXC memory and unusable
current timestamps are freshness-unknown. Non-Proxmox unannotated snapshots,
independently current CPU/network/memory and stored observations stay unchanged.
The memory presentation policy is shared with the dedicated guest drawer in
`memoryObservation.ts`; it never borrows Last seen, a disk deferral or a snapshot
refresh time. Unknown source/state text is not echoed.
`resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts` pins both memory
and filesystem paths, including invalid numbers/dates, zero, every deferral and
unrelated platforms. `ResourceDetailDrawer.history.test.tsx` exercises the full
reactive drawer with the real History renderer; `GuestDrawer.test.tsx` preserves
the dedicated path. `browser-tests/resource-history-provenance.cjs` verifies
these production components and clients with synthetic observations on desktop
and touch WebKit. These are presentation checks, not independent thaw, native
filesystem writes, safe service resumption or release/installed acceptance.
### Observed CPU labels preserve small positives
Shared responsive CPU cells distinguish actual zero from small positive readings:
@@ -140,7 +163,6 @@ readable unmatched explanation and same-resource recovery. No native backup
identity, restore, guest thaw or containing-release acceptance follows from
these source/render observations.
### Diagnostics are live checks; downloads require a privacy review
The existing Settings Diagnostics panel explains before either run action that
@@ -373,7 +395,6 @@ or touch retry, source isolation and actual new organisation headers. Synthetic
HTTP results establish frontend truthfulness/lifecycle, not native PBS/PVE
permission, archive restorability, guest thaw or released availability.
### Composed operator tables and conservative rollback consent
Phone storage tables use the canonical weighted-width helper and matching
@@ -403,7 +424,6 @@ controller/storage/backup/workload tests and production-CSS browser fixture
source at desktop, intermediate and phone widths. Synthetic HTTP/snapshots do
not establish native collectors, tenancy, installation recovery or release delivery.
### Organisation-scoped single-metric History
Fetched HistoryChart observations belong to the active organisation as well as
@@ -996,7 +1016,6 @@ display label or permission to describe every preview as release-ready.
Automatic stable updates remain unavailable while Preview is selected, and
that manual-channel consequence must stay visible at desktop and narrow widths.
### Backup date uncertainty uses existing age cells
Proxmox Coverage and By date age cells label unorderable restore-point dates
@@ -1210,7 +1229,6 @@ Docker table tests and the desktop/touch-phone production-component browser
fixture check these priorities, rather than treating an old percentage as an
immutable primitive contract. No new public route or navigation entry is added.
### Maintenance schedule copy
Operator maintenance banners show local absolute start and end timestamps,
@@ -1241,7 +1259,6 @@ renderers with an edited input retained across overlapping windows. The browser
journey is a narrow Proxmox node Manage form, scrolling to lifecycle Save while
the synthetic estate continues updating, plus the alert timeline and note form.
### PBS host history correlation
The Backups surface passes its complete deduplicated route inventory to the PBS table. Only PBS servers render as rows; other resources supply correlation inputs. Preserve PBS drawer identity and use the correlated canonical history target. Missing disk utilisation does not gate CPU/memory history.
@@ -4070,7 +4087,6 @@ Cached update responses remain valid without a release date. Browser qualificati
covers the current settings route, pending/retry states, keyboard checks and date
presentation at desktop, intermediate and narrow widths.
### Provider tabs use compact canonical evidence during route hydration
Evidence-gated provider tabs consume the unified-resource owner's
@@ -15,6 +15,18 @@
## Purpose
Drawer observation labels derive locally from the selected snapshot without renewing timestamps, adding reads, remounting charts or changing stored metric coordinates.
### Drawer provenance stays local to snapshot presentation
Guest and canonical resource drawer memory presentation share the same pure
original-observation policy. Same-ID state replacement updates current versus
retained/unknown fallback labels without remounting a chart, changing coordinates,
adding reads or plotting live numbers as stored observations. The full
`GuestDrawer.test.tsx` and `ResourceDetailDrawer.history.test.tsx` controls assert
unchanged single-read budgets through retained-to-current replacement and zero
preservation. This changes no estate collection, timing target or cache policy.
### Bulk resource reads do not repeatedly derive canonical metadata
Registry List and typed-view copies materialize identity/scopes/policy once
@@ -33,7 +45,6 @@ moving that work into the registry is not proof that total ingest or broadcast
CPU improved. Native #2199 CPU/RSS, trailing refresh/duplicate ingest identity,
owner-set grouping, broadcast conversion and the sawtooth remain separate.
### Agent identity lookup cost follows identity-relevant inventory
Report admission no longer clones/ingests twenty explicitly listed Kubernetes
@@ -51,7 +62,6 @@ compared without selecting a favourable rerun. This is a bounded source cost
improvement, not attribution of the reporter's native CPU, RSS, sawtooth or
remaining refresh/grouping/broadcast mechanisms.
### Memory presentation does not collect or renew observations
Guest-memory provenance is projected from the already-owned snapshot in the
@@ -149,7 +159,6 @@ clearing, false/zero values, unchanged peer-site rows and full/fast equivalence.
merges, row, Overview and History with synthetic inputs. This is no native
QGA/thaw, installed recovery, fleet-performance or release-availability claim.
### Unavailable filesystem values share the existing disk model
The filesystem list and guest Overview use the same nullable measured-byte ratio
@@ -218,7 +227,6 @@ ownership remain unchanged. Guest column regressions verify the sums and the
production composed-table browser fixture checks identity and all configured
metric tracks at narrow width; this is readability, not a throughput claim.
### Bounded confidence/priority work in broadcast grouping — issue #2199
The general identity matcher retains every confidence, reason, ordering and
@@ -1754,7 +1762,6 @@ final measurements and host-load limitation remain recorded in
`records/resource-payload-static-metadata-2026-08-24.md`. They do not establish
a controlled performance improvement or satisfy the open SLO qualification.
### Large API responses negotiate gzip without corrupting edge cases
The main listener compresses eligible JSON and frontend text assets for
@@ -3551,7 +3558,6 @@ The signal survives transport loss and resets on organisation URL changes.
On admission-request failure retain the existing facet without extra retries,
polling or full-estate reads. A later successful refresh replaces that facet.
### ASCII route-label classification
HTTP metric route normalization classifies numeric IDs and UUID-like labels
@@ -23,6 +23,26 @@ and sort the complete canonical change table while startup and ingestion wait.
## Purpose
Canonical resource History honours selected memory observation provenance and Proxmox VM filesystem deferrals instead of treating numeric carriers as fresh measurements.
### Canonical resource History cannot promote retained guest readings
`ResourceMetric.current` is a selected numeric carrier, not a freshness
assertion. Canonical drawer History consumes its existing memory observation
and Proxmox VM filesystem read reason as independent evidence, just as the
dedicated guest drawer does. Current memory requires a usable original time
when annotated; last-known/unknown/unavailable evidence has explicit labels.
A VM read deferral withdraws current disk fallback, retaining a number only
for the existing `prev-` reason vocabulary. LXC disk usage is not a QEMU
filesystem read. Other platform/catalog metrics and capacity aliases stay
unchanged. Same-ID replacement updates this presentation without remounting
History or requesting another read. Clearing deferral is not native recovery.
The full drawer regression in `ResourceDetailDrawer.history.test.tsx` covers
retention, unavailable carriers, original timestamps, measured zero and recovery
without invented chart points; model and browser controls retain source/access
isolation. No wire schema, collector, alert, request identity or authority changes.
### Bulk registry copies reuse mutation-bound canonical metadata
`ResourceRegistry.List` and typed-view rebuilding share canonical identity,
@@ -49,7 +69,6 @@ lazy views, races and allocation cost. The external connected demo test includes
all nine providers and its metadata-heavy estate. These are source invariants,
not native fleet CPU/RSS relief, backup recovery or release availability.
### Container CPU presentation explains its existing scale
The existing Docker/Podman container table and drawer visibly explain that CPU
@@ -199,7 +218,6 @@ measured zero, partial omissions and full/delta/fast recovery. The connected
and History presentation in desktop Chromium and phone WebKit. Synthetic
source proof does not establish native backup safety or shipped availability.
### Backup server counts retain their inventory availability
The existing PBS server/datastore row takes backup-inventory read availability
@@ -231,7 +249,6 @@ The shared presentation and mounted regressions plus the production-component
`browser-tests/guest-disk-deferral.cjs` fixture verify this client behaviour;
they do not prove native QGA coordination, History freshness or delivery.
### Narrow controller and backup summaries retain their source facts
Kubernetes controller row expansions expose supplied kind-specific targets,
@@ -248,7 +265,6 @@ hover and search. The recovery-model, table and guest regressions plus desktop/
phone browser rendering verify attribution, visible facts and source composition.
These frontend improvements change no collector, API authority or native status.
### Resource evidence fallback access boundaries
When the existing resource-facet reader receives a final HTTP 401/403, its
@@ -437,7 +453,6 @@ read-only incident memory association. It does not rewrite event resource IDs,
resource operator state, approvals, action request identity or execution
capabilities. An unavailable canonical read remains an error.
### Canonical Patrol and Assistant continuation, 2026-09-07
Persisted action request identity is scoped to the trusted actor and exact
@@ -662,7 +677,6 @@ distinction afterwards. The unified risk projection gates wearout on
`storagehealth.WearoutReported` so its verdict and the server-side alert verdict
about the same disk cannot diverge.
### Coverage rows preserve unknown backup chronology
The existing Proxmox backup Coverage projection carries unknown-date flags
@@ -875,7 +889,6 @@ timestamp retained. Native table/model/guardrail tests and the production
`browser-tests/proxmox-docker-rows.cjs` exercise these surfaces without executing
a container action or claiming native appliance or release acceptance.
### PBS host history correlation
PBS presentation correlation accepts standalone agents and VM/system-container resources with an agent facet. Preserve unique identity matching and canonical metricsTarget type and ID. Guests without telemetry and mixed ambiguous agent/guest matches must not replace the PBS target.
@@ -0,0 +1,303 @@
// Fresh combined-content proof, not native Podman or alert-delivery acceptance.
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/docker-composed-20261005-controls';
const origin = 'http://127.0.0.1:5341';
const runtimePath = 'frontend-modern/src/features/docker/DockerContainersTable.tsx';
const expectedHash = '85753788c2e982dcedbab9aed99ff74a1f1623ec35bc7886ab25661e86de2dd9';
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
(async () => {
fs.mkdirSync(output, { recursive: true });
assert.equal(hash('/workspace/' + runtimePath), expectedHash);
const result = {
result: 'incomplete',
playwright: require('playwright/package.json').version,
content_sha256: { [runtimePath]: expectedHash },
cases: [],
screenshots: [],
limits:
'Production table, drawer and CSS with selected synthetic resources/APIs. Emulated phone, not a native phone. No collector, engine, threshold duration, notification, 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: { noDiscovery: true, include: [] },
server: { host: '127.0.0.1', port: 5341, strictPort: true, watch: null },
});
let browser;
try {
await server.listen();
for (const [name, engine, width, dark] of [
['desktop-light', chromium, 1440, false],
['desktop-dark', chromium, 1440, true],
['phone-light', webkit, 390, false],
['phone-dark', webkit, 390, true],
]) {
browser = await engine.launch(
engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
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(12000);
page.setDefaultNavigationTimeout(60000);
const record = {
name,
width,
dark,
browser: browser.version(),
errors: [],
requests: [],
checks: [],
};
result.cases.push(record);
page.on('pageerror', (error) => record.errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') record.errors.push(message.text());
});
await page.routeWebSocket('**/*', (socket) => {
if (socket.url().startsWith('ws://127.0.0.1:5341/')) socket.connectToServer();
else {
record.requests.push({ blocked: true, socket: socket.url() });
socket.close();
}
});
await page.route('**/*', (route) => {
const request = route.request(),
url = new URL(request.url());
if (url.origin !== origin || request.method() !== 'GET') {
record.requests.push({ blocked: true, url: request.url(), method: request.method() });
return route.abort();
}
if (!url.pathname.startsWith('/api/')) return route.continue();
record.requests.push({ path: url.pathname, method: request.method() });
return route.fulfill({
json: {
success: true,
data: [],
alerts: [],
anomalies: [],
config: null,
capabilities: [],
relationships: [],
recentChanges: [],
available: false,
audits: [],
count: 0,
blocked_capabilities: [],
limits: [],
max_history_days: 90,
},
});
});
await page.addInitScript((dark) => {
const apply = () => document.documentElement.classList.toggle('dark', dark);
if (document.documentElement) apply();
else document.addEventListener('DOMContentLoaded', apply, { once: true });
window.__inputs = [];
for (const type of ['click', 'keydown'])
document.addEventListener(
type,
(event) => window.__inputs.push({ type, trusted: event.isTrusted }),
true,
);
}, dark);
await page.goto(origin + '/browser-tests/docker-composed.html', { waitUntil: 'networkidle' });
const activate = async (locator) => {
if (width < 768) await locator.tap();
else {
await locator.focus();
await page.keyboard.press('Enter');
}
};
const table = page.getByRole('table');
const cpuCell = async (id) => {
const index = await table
.locator('thead th')
.evaluateAll((headers) =>
headers.findIndex((header) => header.textContent.trim() === 'CPU'),
);
assert.ok(index >= 0, JSON.stringify(await table.locator('thead th').allTextContents()));
return page.locator(`[data-docker-container-row="${id}"]`).getByRole('cell').nth(index);
};
const labels = async () => {
for (const [id, text] of [
['small-positive', '0.3%'],
['measured-zero', '0%'],
['tiny-positive', '<0.1%'],
]) {
const cell = await cpuCell(id);
await cell.getByText(text, { exact: true }).waitFor();
}
};
const screenshot = async (state) => {
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
});
assert.ok(
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1),
'No document overflow',
);
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)) });
};
await page
.locator('[data-docker-host-group="edge-a"] [data-docker-host-group-stale]')
.waitFor();
assert.equal(
await page.locator('[data-docker-host-group-stale]').innerText(),
'No report for 9m',
);
assert.match(
await page.locator('[data-docker-host-group-stale]').getAttribute('title'),
/last values received/,
);
assert.equal(
await table
.locator('thead th')
.filter({ hasText: /^Host$/ })
.count(),
0,
);
assert.equal(
await table
.locator('thead th')
.filter({ hasText: /^Engine$/ })
.count(),
0,
);
assert.match(
await page.locator('[data-docker-host-group="edge-a"]').innerText(),
/docker fixture/,
);
assert.match(
await page.locator('[data-docker-host-group="edge-b"]').innerText(),
/podman fixture/,
);
assert.equal(
await page
.locator('[data-docker-host-group="edge-b"] [data-docker-host-group-stale]')
.count(),
0,
);
await labels();
const guidance = page.getByTestId('container-table-cpu-scale');
assert.match(await guidance.innerText(), /100% means all host CPUs.*per-core scale/);
await screenshot('grouped-with-hosts');
record.checks.push(
'Both fractional labels and shared host/engine headings survive composition; only the silent host is qualified.',
);
await activate(page.getByRole('button', { name: 'Remove host context', exact: true }));
assert.equal(await page.locator('[data-docker-host-group-stale]').count(), 0);
assert.match(
await page.locator('[data-docker-host-group="edge-a"]').innerText(),
/docker fixture/,
);
await labels();
await screenshot('grouped-without-hosts');
record.checks.push(
'Missing host context removes stale/status claims, keeps container engine fallback, fractions and scale guidance.',
);
await activate(page.getByRole('button', { name: 'Restore host context', exact: true }));
if (width < 768) await activate(page.getByRole('button', { name: 'Filters', exact: true }));
await activate(page.getByRole('button', { name: 'View', exact: true }));
await activate(page.getByRole('button', { name: 'List', exact: true }));
await page.keyboard.press('Escape');
assert.equal(await page.locator('[data-docker-host-group]').count(), 0);
if (width >= 1024) {
assert.equal(
await table
.locator('thead th')
.filter({ hasText: /^Host$/ })
.count(),
1,
);
assert.equal(
await table
.locator('thead th')
.filter({ hasText: /^Engine$/ })
.count(),
1,
);
}
await labels();
const containerRow = page.locator('[data-docker-container-row="small-positive"]');
// On phones the production disclosure button is intentionally hidden;
// use its existing whole-row pointer convenience, not a forced click.
if (width < 768) await containerRow.tap();
else await activate(containerRow.getByRole('button').first());
const detail = page.locator('[data-inline-platform-resource-detail-for="small-positive"]');
await detail.waitFor();
assert.match(
await detail.getByTestId('container-drawer-cpu-scale').innerText(),
/total host capacity/,
);
await screenshot('flat-expanded');
await activate(
detail.getByRole('button', { name: 'Collapse small-positive details', exact: true }),
);
await detail.waitFor({ state: 'detached' });
record.checks.push(
'Flat layout retains CPU labels; trusted expansion/collapse shows the same capacity guidance in the real drawer.',
);
assert.deepEqual(record.errors, []);
assert.ok(!record.requests.some((request) => request.blocked));
record.trustedInputs = await page.evaluate(() => window.__inputs);
assert.ok(record.trustedInputs.some((event) => event.trusted));
await browser.close();
browser = undefined;
}
result.result = 'passed';
result.verified_at = new Date().toISOString();
} catch (error) {
result.error = String(error.stack || error);
const failedPage = browser?.contexts()[0]?.pages()[0];
if (failedPage) {
result.failure_headers = await failedPage.locator('thead th').allTextContents();
result.failure_body = (await failedPage.locator('body').innerText()).slice(0, 5000);
await failedPage.screenshot({ path: output + '/failure.png', fullPage: true });
}
throw error;
} finally {
if (browser) await browser.close();
await server.close();
result.browser_closed = true;
result.server_closed = true;
fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
}
process.stdout.write(
JSON.stringify({
result: result.result,
states: result.cases.length,
screenshots: result.screenshots.length,
resultPath: output + '/result.json',
}) + '\n',
);
})().catch((error) => {
process.stderr.write(String(error.stack || error) + '\n');
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>Composed container presentation proof</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/docker-composed.tsx"></script>
</body>
</html>
@@ -0,0 +1,87 @@
// Presentation only: selected synthetic resources, real table, drawer and CSS.
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import { Router, Route } from '@solidjs/router';
import { DockerContainersTable } from '../src/features/docker/DockerContainersTable';
import { DarkModeContext } 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 });
const container = (name: string, host: string, cpu: number, engine = 'docker'): Resource => ({
id: name,
type: 'app-container',
name,
platformId: name,
platformType: 'docker',
sourceType: 'agent',
status: 'running',
cpu: { current: cpu },
memory: { current: 10 },
docker: { hostname: host, runtime: engine, runtimeVersion: 'fixture', containerState: 'running' },
});
const containers = [
container('small-positive', 'edge-a', 0.26),
container('measured-zero', 'edge-a', 0),
container('tiny-positive', 'edge-b', 0.04, 'podman'),
];
const hosts: Resource[] = [
{
id: 'host-a',
type: 'docker-host',
name: 'edge-a',
platformId: 'host-a',
platformType: 'docker',
sourceType: 'agent',
status: 'offline',
docker: { hostname: 'edge-a', runtime: 'docker', runtimeVersion: 'fixture' },
health: {
verdict: 'stale',
reasons: [{ code: 'telemetry_stale', detail: '9m' }],
},
},
{
id: 'host-b',
type: 'docker-host',
name: 'edge-b',
platformId: 'host-b',
platformType: 'docker',
sourceType: 'agent',
status: 'online',
docker: { hostname: 'edge-b', runtime: 'podman', runtimeVersion: 'fixture' },
},
];
function Fixture() {
const [withHosts, setWithHosts] = createSignal(true);
return (
<DarkModeContext.Provider value={() => document.documentElement.classList.contains('dark')}>
<main class="min-h-screen space-y-4 bg-surface p-3 text-base-content">
<h1 class="text-lg font-semibold">Composed container presentation</h1>
<p class="text-xs text-muted">Synthetic metrics. No container or notification action.</p>
<button
type="button"
class="min-h-11 rounded border border-border px-3 text-xs"
onClick={() => setWithHosts(!withHosts())}
>
{withHosts() ? 'Remove host context' : 'Restore host context'}
</button>
<DockerContainersTable
resources={containers}
hosts={withHosts() ? hosts : undefined}
emptyIcon={<span />}
emptyTitle="No containers"
emptyDescription="No containers"
/>
</main>
</DarkModeContext.Provider>
);
}
render(
() => (
<Router>
<Route path="*" component={Fixture} />
</Router>
),
document.getElementById('root')!,
);
@@ -0,0 +1,400 @@
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/history-provenance-20261005-complete/browser';
const origin = 'http://127.0.0.1:5317';
const runtime = [
'frontend-modern/src/utils/memoryObservation.ts',
'frontend-modern/src/components/Workloads/guestDrawerModel.ts',
'frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts',
'frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts',
'frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx',
];
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)])),
cases: [],
screenshots: [],
limits:
'Production ResourceDetailDrawer and GuestDrawer, History, client and CSS; synthetic local observations/APIs. Not native guest safety, provider/appliance, installed, reporter 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: { noDiscovery: true, include: [] },
server: { host: '127.0.0.1', port: 5317, strictPort: true, watch: null },
});
let browser;
try {
await server.listen();
for (const [name, engine, width, dark, guest] of [
['resource-desktop-light', chromium, 1365, false, false],
['resource-desktop-dark', chromium, 1365, true, false],
['resource-phone-dark', webkit, 390, true, false],
['resource-phone-light', webkit, 390, false, false],
['guest-desktop-light', chromium, 1365, false, true],
['guest-desktop-dark', chromium, 1365, true, true],
['guest-phone-dark', webkit, 390, true, true],
['guest-phone-light', webkit, 390, false, true],
]) {
browser = await engine.launch(
engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
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(12000);
page.setDefaultNavigationTimeout(60000);
const record = {
name,
width,
dark,
guest,
browser: browser.version(),
errors: [],
consoleErrors: [],
expectedHttpFailures: [],
requests: [],
checks: [],
};
result.cases.push(record);
let mode = 'empty';
const start = Date.parse('2026-10-04T12:00:00Z'),
end = Date.parse('2026-10-05T00:00:00Z');
const point = (timestamp, value) => ({ timestamp, value, min: value, max: value });
page.on('pageerror', (error) => record.errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error')
record.consoleErrors.push({ text: message.text(), url: message.location().url });
});
await page.routeWebSocket('**/*', (socket) => {
if (socket.url().startsWith('ws://127.0.0.1:5317/')) socket.connectToServer();
else {
record.requests.push({ blocked: true, socket: socket.url() });
socket.close();
}
});
await page.route('**/*', (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();
assert.equal(route.request().method(), 'GET');
record.requests.push({ path: url.pathname, method: route.request().method(), mode });
if (url.pathname === '/api/metrics-store/history') {
assert.equal(url.searchParams.get('resourceType'), 'vm');
assert.equal(url.searchParams.get('resourceId'), 'fixture:pve:101');
assert.equal(url.searchParams.get('maxPoints'), '240');
if (mode === 'transient' || mode === 'denied') {
record.expectedHttpFailures.push({
url: url.href,
status: mode === 'transient' ? 503 : 403,
});
return route.fulfill({
status: mode === 'transient' ? 503 : 403,
json: { error: 'private-error-sentinel' },
});
}
return route.fulfill({
json: {
resourceType: 'vm',
resourceId: 'fixture:pve:101',
range: url.searchParams.get('range'),
start,
end,
source: 'store',
metrics:
mode === 'stored'
? {
memory: [point(start, 5), point(end, 0)],
disk: [point(start, 40), point(end, 45)],
}
: {},
},
});
}
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),
);
window.__inputs = [];
for (const type of ['click', 'keydown', 'input'])
document.addEventListener(
type,
(e) => window.__inputs.push({ type, trusted: e.isTrusted }),
true,
);
}, dark);
await page.goto(
origin + '/browser-tests/resource-history-provenance.html' + (guest ? '?guest' : ''),
);
const activate = (locator) => (width < 768 ? locator.tap() : locator.click());
await activate(page.getByRole('tab', { name: 'History', exact: true }));
const chart = page.locator('[data-history-group="utilization"]');
await chart.locator('[data-history-last-known="memory"]').waitFor();
await page.evaluate(
() => (window.__chart = document.querySelector('[data-history-group="utilization"]')),
);
const screenshot = 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, sha256: hash(path.join(output, file)), state, name });
const geometry = await page.evaluate(() => ({
width: document.documentElement.clientWidth,
scroll: document.documentElement.scrollWidth,
}));
assert.ok(geometry.scroll <= geometry.width + 1, JSON.stringify(geometry));
};
assert.match(
await chart.locator('[data-history-last-known="memory"]').innerText(),
/25.0%\s*last known/,
);
assert.match(
await chart.locator('[data-history-last-known="disk"]').innerText(),
/50.0%\s*last known/,
);
assert.equal(
await chart
.locator('[data-history-current="memory"], [data-history-current="disk"]')
.count(),
0,
);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/2026-10-04 12:00:00 UTC/,
);
assert.match(
await chart.locator('[data-history-deferred="disk"]').innerText(),
/operation lock/,
);
assert.equal(await chart.locator('path, [data-history-observation]').count(), 0);
const description = await chart
.locator('[data-history-last-known="memory"]')
.evaluate((el) => document.getElementById(el.getAttribute('aria-describedby')).textContent);
assert.match(description, /Not a current measurement/);
await screenshot('retained');
record.checks.push(
'Retained memory/disk qualified visibly and accessibly; no synthetic stored points.',
);
const reads = () =>
record.requests.filter((r) => r.path === '/api/metrics-store/history').length;
const firstReads = reads();
await activate(page.getByRole('button', { name: 'Advance snapshot', exact: true }));
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/2026-10-04 12:00:00 UTC/,
);
assert.equal(reads(), firstReads);
record.checks.push(
'Snapshot Last seen refresh cannot renew observation time or add a History read.',
);
await activate(page.getByRole('button', { name: 'unavailable', exact: true }));
assert.equal(
await chart
.locator(
'[data-history-last-known], [data-history-current="memory"], [data-history-current="disk"]',
)
.count(),
0,
);
assert.match(await chart.innerText(), /Memory\s*-/);
assert.match(await chart.innerText(), /Disk\s*-/);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/Unavailable/,
);
await screenshot('unavailable');
record.checks.push(
'Unavailable memory and non-retained filesystem carriers are missing, never zero/current.',
);
await activate(page.getByRole('button', { name: 'unknown', exact: true }));
assert.match(
await chart.locator('[data-history-unknown="memory"]').innerText(),
/25.0%\s*freshness unknown/,
);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/Unknown source.*Observation time unknown/,
);
assert.doesNotMatch(
await page.locator('body').innerText(),
/private-(state|source|error)-sentinel|2999/,
);
record.checks.push(
'Unknown state/source/future timestamp fail closed without leaking raw text.',
);
await activate(page.getByRole('button', { name: 'current', exact: true }));
assert.match(
await chart.locator('[data-history-current="memory"]').innerText(),
/0.0%\s*current/,
);
assert.match(
await chart.locator('[data-history-current="disk"]').innerText(),
/50.0%\s*current/,
);
assert.equal(await chart.locator('[data-history-deferred]').count(), 0);
assert.equal(reads(), firstReads);
assert.ok(
await page.evaluate(
() => window.__chart === document.querySelector('[data-history-group="utilization"]'),
),
);
await screenshot('current');
record.checks.push(
'Fresh measured zero and independent disk recovery render in place without extra reads or inferred thaw.',
);
mode = 'stored';
await activate(page.getByRole('button', { name: 'Refresh history', exact: true }));
const slider = chart.getByRole('slider', { name: 'Inspect Utilization history' });
await slider.waitFor();
assert.equal(
await chart
.locator('[data-history-current="memory"], [data-history-current="disk"]')
.count(),
0,
);
await slider.focus();
await page.keyboard.press('Home');
assert.match(await slider.getAttribute('aria-valuetext'), /Memory 5.0%.*Disk 40.0%/);
await page.keyboard.press('End');
assert.match(await slider.getAttribute('aria-valuetext'), /Memory 0.0%.*Disk 45.0%/);
assert.equal(await chart.locator('path').count(), 2);
await screenshot('stored');
await slider.blur();
await activate(page.getByRole('button', { name: 'retained', exact: true }));
assert.equal(await chart.locator('path').count(), 2);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/Last known/,
);
record.checks.push(
'Actual stored history and trusted Home/End inspection remain separate from live/retained provenance.',
);
mode = 'transient';
await activate(page.getByRole('button', { name: 'Refresh history', exact: true }));
await page.getByRole('button', { name: 'Retry history', exact: true }).waitFor();
assert.equal(await chart.locator('path').count(), 2);
assert.match(
await page.getByRole('status', { name: 'History refresh status' }).innerText(),
/previously loaded history/,
);
mode = 'denied';
await activate(page.getByRole('button', { name: 'Retry history', exact: true }));
await chart.waitFor({ state: 'detached' });
assert.match(await page.locator('body').innerText(), /Access denied/);
assert.equal(
await page
.locator('[data-history-deferred], [data-history-last-known], [data-history-current]')
.count(),
0,
);
assert.doesNotMatch(
await page.locator('body').innerText(),
/private-(state|source|error)-sentinel/,
);
record.checks.push(
'Transient read retains only stored evidence; access denial withdraws all History carriers.',
);
assert.deepEqual(record.errors, []);
// Chromium reports deliberately failed fetches to its console; WebKit
// need not. Retain every message and permit only the two exact endpoint
// and status pairs exercised above, never a general error filter.
assert.equal(record.expectedHttpFailures.length, 2);
for (const error of record.consoleErrors) {
assert.ok(
record.expectedHttpFailures.some(
(failure) =>
error.url === failure.url &&
error.text ===
`Failed to load resource: the server responded with a status of ${failure.status} (${failure.status === 503 ? 'Service Unavailable' : 'Forbidden'})`,
),
JSON.stringify(error),
);
}
assert.ok(!record.requests.some((r) => r.blocked));
const inputs = await page.evaluate(() => window.__inputs);
assert.ok(inputs.some((e) => e.type === 'keydown' && e.trusted));
assert.ok(inputs.some((e) => e.type === 'click' && e.trusted));
record.trustedInputs = inputs;
await browser.close();
browser = undefined;
}
result.result = 'passed';
result.verified_at = new Date().toISOString();
} catch (error) {
result.error = String(error.stack || error);
throw error;
} finally {
if (browser) await browser.close();
await server.close();
result.browser_closed = true;
result.server_closed = true;
fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
}
process.stdout.write(
JSON.stringify({
result: result.result,
groups: result.cases.length,
checks: result.cases.reduce((n, c) => n + c.checks.length, 0),
screenshots: result.screenshots.length,
resultPath: output + '/result.json',
}) + '\n',
);
})().catch((e) => {
process.stderr.write(String(e.stack || e) + '\n');
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>Resource History provenance verification</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/resource-history-provenance.tsx"></script>
</body>
</html>
@@ -0,0 +1,137 @@
// Synthetic snapshots only; both drawers, History, transport and CSS are production.
import { createMemo, createSignal } 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 { DarkModeContext } from '../src/contexts/appRuntime';
import { syncAIRuntimeSettings } from '../src/stores/aiRuntimeState';
import type { Resource } from '../src/types/resource';
import type { WorkloadGuest } from '../src/types/workloads';
import '../src/index.css';
syncAIRuntimeSettings({ discovery_enabled: false });
function Fixture() {
const [state, setState] = createSignal('retained');
const [seen, setSeen] = createSignal(Date.parse('2026-10-04T13:00:00Z'));
const value = () => (state() === 'current' || state() === 'unavailable' ? 0 : 25);
const observation = () => ({
state:
state() === 'current'
? 'current'
: state() === 'unavailable'
? 'unavailable'
: state() === 'unknown'
? 'private-state-sentinel'
: 'last-known',
source:
state() === 'unknown'
? 'private-source-sentinel'
: state() === 'current'
? 'status-mem'
: 'guest-agent-meminfo',
observedAt: state() === 'unknown' ? '2999-01-01T00:00:00Z' : '2026-10-04T12:00:00Z',
});
const reason = () =>
state() === 'current' ? '' : state() === 'unavailable' ? 'agent-busy' : 'prev-vm-locked';
const resource = createMemo<Resource>(() => ({
id: 'fixture-vm',
type: 'vm',
name: 'backup-guest',
displayName: 'Backup guest',
platformId: 'fixture',
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'running',
lastSeen: seen(),
metricsTarget: { resourceType: 'vm', resourceId: 'fixture:pve:101' },
cpu: { current: 10 },
memory: { current: value(), total: 100, used: value(), observation: observation() },
disk: { current: 50 },
network: { rxBytes: 0, txBytes: 200 },
proxmox: {
vmid: 101,
node: 'pve',
instance: 'fixture',
diskStatusReason: reason(),
backupInProgress: Boolean(reason()),
},
}));
const guest = createMemo<WorkloadGuest>(() => ({
id: 'fixture:pve:101',
vmid: 101,
node: 'pve',
instance: 'fixture',
name: 'Backup guest',
type: 'qemu',
status: 'running',
cpu: 0.1,
cpus: 2,
memory: {
total: 100,
used: value(),
free: 100 - value(),
usage: value(),
observation: observation(),
},
disk: { total: 100, used: 50, usage: 50 },
diskStatusReason: reason(),
networkIn: 0,
networkOut: 200,
diskRead: 0,
diskWrite: 0,
uptime: 3600,
template: false,
lastBackup: 0,
tags: [],
lastSeen: new Date(seen()).toISOString(),
lock: reason() ? 'backup' : '',
}));
return (
<main class="min-h-screen bg-surface p-3 text-base-content">
<h1 class="text-base font-semibold">Resource History provenance</h1>
<p class="my-2 text-xs text-muted">
Synthetic observations. No backup or guest-agent operation is sent.
</p>
<div class="mb-3 flex flex-wrap gap-2" aria-label="Fixture observation controls">
{['retained', 'unavailable', 'unknown', 'current'].map((s) => (
<button
class="min-h-11 rounded-sm border border-border px-3 text-xs"
onClick={() => setState(s)}
>
{s}
</button>
))}
<button
class="min-h-11 rounded-sm border border-border px-3 text-xs"
onClick={() => setSeen(Date.parse('2026-10-05T00:00:00Z'))}
>
Advance snapshot
</button>
</div>
<section class="mx-auto max-w-5xl" aria-label="Production drawer">
{new URLSearchParams(location.search).has('guest') ? (
<GuestDrawer guest={guest()} onClose={() => {}} />
) : (
<ResourceDetailDrawer resource={resource()} presentation="table-row" />
)}
</section>
</main>
);
}
render(
() => (
<Router>
<Route
path="*"
component={() => (
<DarkModeContext.Provider
value={() => document.documentElement.classList.contains('dark')}
>
<Fixture />
</DarkModeContext.Provider>
)}
/>
</Router>
),
document.getElementById('root')!,
);
@@ -0,0 +1,54 @@
{
"version": 1,
"base_sha": "c0f0474423869e5ccc3d59db2d3013669ac37215",
"verified_at": "2026-10-05T05:35:15.985Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx",
"frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts",
"frontend-modern/src/components/Workloads/guestDrawerModel.ts",
"frontend-modern/src/utils/memoryObservation.ts"
],
"content_sha256": {
"frontend-modern/src/utils/memoryObservation.ts": "12a617cbd1af52345fa34a0ef79ba10a31675c07e5c974e28f1158a39f93c9a4",
"frontend-modern/src/components/Workloads/guestDrawerModel.ts": "b681ee7b391dfa328be61689a2fe10a040b7e06a01985f893d9780204aa89188",
"frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts": "464c18ed3fefdbf0332e54519be9c8ffa95a5e95e84c8e71eed50f27ddd4c661",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "48eaffc83be59c0e5373ad938d010015eb34b24e2bf65aaa07315e3c465c5a68",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "1d0f2261db7dc56b389f2e7bd43ce822c6b4689324d74ff1877c2c1a1eeb4863"
},
"routes": [
"/browser-tests/resource-history-provenance.html (production canonical ResourceDetailDrawer)",
"/browser-tests/resource-history-provenance.html?guest (production dedicated GuestDrawer)"
],
"viewports": [
{
"width": 1365,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Eight Chromium desktop / touch WebKit emulated-phone light/dark cases, Playwright 1.56.1 matching integration lock. All 32 full-page captures independently hash-matched and visually inspected.",
"Retained memory and filesystem values labelled last known; original source/time, unavailable versus measured zero, unknown state/source/future timestamp and unrelated current readings stay distinct. No invented stored samples.",
"Same-ID snapshot refresh cannot renew an observation time, remount History or request another read. Actual stored observations keep their coordinates/keyboard inspection.",
"Deliberate 503 retains loaded stored evidence and 403 withdraws History carriers. All negative-response console messages retained and asserted against exact exercised endpoint/status pairs; no unexpected errors, off-origin or write requests."
],
"interactions": [
"Select History with trusted desktop pointer or touch input; replace retained/unavailable/unknown/current synthetic observations.",
"Advance snapshot Last seen; verify unchanged original observation and read count; recovery preserves measured zero without asserting native thaw.",
"Refresh stored history, use trusted keyboard Home/End inspection; refresh with synthetic transient failure then Retry into denied access."
],
"proof": {
"script": "frontend-modern/browser-tests/resource-history-provenance.cjs",
"script_sha256": "4b5dc0280bc25941a4f8193ca8db59cddb3b09abd09118f41568f0be05aa6d0b",
"checks": 56,
"screenshots": 32,
"browser_closed": true,
"server_closed": true,
"limits": "Production ResourceDetailDrawer and GuestDrawer, History, client and CSS; synthetic local observations/APIs. Not native guest safety, provider/appliance, installed, reporter or release acceptance."
}
}
@@ -0,0 +1,43 @@
{
"version": 1,
"base_sha": "ca255a0e3f81a8d1588f3e783b7084fc863b37c8",
"verified_at": "2026-10-05T05:44:42.674Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/docker/DockerContainersTable.tsx"
],
"content_sha256": {
"frontend-modern/src/features/docker/DockerContainersTable.tsx": "85753788c2e982dcedbab9aed99ff74a1f1623ec35bc7886ab25661e86de2dd9"
},
"routes": [
"/browser-tests/docker-composed.html (production DockerContainersTable and ResourceDetailDrawer)"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Four Chromium desktop / touch WebKit emulated-phone light/dark cases; Playwright 1.56.1 matches integration lock. All 12 captures independently hash-matched and visually inspected.",
"Combined small-positive/zero/tiny-positive CPU labels with silent-host/shared-column presentation. Available and missing host context; grouped and flat layout preserve labels and total-host-capacity guidance.",
"Real drawer expansion/collapse uses trusted keyboard desktop and tap phone input. View selection follows actual mobile Filters disclosure. No unexpected errors, off-origin requests or operation requests."
],
"interactions": [
"Toggle host context and restore it; verify host status and engine fallback separately.",
"Open real View disclosure (phone Filters first), select List, expand and collapse small-positive container; inspect real drawer scale explanation."
],
"proof": {
"script": "frontend-modern/browser-tests/docker-composed.cjs",
"script_sha256": "7529007d411f8d40bda969e2b43470779077c263e704aba43ecba34fc21467e8",
"checks": 12,
"screenshots": 12,
"browser_closed": true,
"server_closed": true,
"limits": "Production table, drawer and CSS with selected synthetic resources/APIs. Emulated phone, not a native phone. No collector, engine, threshold duration, notification, installed or release acceptance."
}
}
@@ -202,6 +202,7 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
target={target()}
range={drawer.metricsHistoryRange()}
currentMetrics={drawer.metricsHistoryCurrentMetrics()}
deferredMetrics={drawer.metricsHistoryDeferredMetrics()}
groups={drawer.metricsHistoryGroups()}
/>
</div>
@@ -6,7 +6,9 @@ import {
within,
waitFor,
} from '@solidjs/testing-library';
import { Suspense } from 'solid-js';
import { createSignal, Suspense } from 'solid-js';
import { ChartsAPI } from '@/api/charts';
import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw';
import discoveryTabStateSource from '@/components/Discovery/useDiscoveryTabState.ts?raw';
@@ -165,6 +167,93 @@ const baseResource = (overrides: Partial<Resource>): Resource => ({
});
describe('ResourceDetailDrawer change history section', () => {
it('keeps retained guest memory and paused filesystems out of current History fallbacks', async () => {
resetCreateNonSuspendingQueryCacheForTest();
const source = baseResource({
id: 'provenance-vm',
type: 'vm',
platformType: 'proxmox-pve',
metricsTarget: { resourceType: 'vm', resourceId: 'fixture:pve:101' },
memory: {
current: 25,
observation: {
state: 'last-known',
source: 'guest-agent-meminfo',
observedAt: '2026-10-04T12:00:00Z',
},
},
disk: { current: 50 },
proxmox: { diskStatusReason: 'prev-vm-locked', backupInProgress: true },
});
const read = vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue({
resourceType: 'vm',
resourceId: 'fixture:pve:101',
range: '24h',
start: Date.parse('2026-10-04T12:00:00Z'),
end: Date.parse('2026-10-05T00: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="memory"]')).toBeNull();
expect(chart.querySelector('[data-history-last-known="memory"]')).toHaveTextContent(
'25.0%last known',
);
expect(chart.querySelector('[data-history-deferred="memory"]')).toHaveTextContent(
'2026-10-04 12:00:00 UTC',
);
expect(chart.querySelector('[data-history-current="disk"]')).toBeNull();
expect(chart.querySelector('[data-history-last-known="disk"]')).toHaveTextContent(
'50.0%last known',
);
setResource({ ...source, lastSeen: Date.parse('2026-10-05T00:00:00Z') });
expect(chart.querySelector('[data-history-deferred="memory"]')).not.toHaveTextContent(
'2026-10-05',
);
setResource({
...source,
memory: { current: 0, observation: { state: 'unavailable', source: 'status-mem' } },
proxmox: { diskStatusReason: 'agent-busy' },
});
expect(chart.querySelector('[data-history-last-known]')).toBeNull();
expect(chart).toHaveTextContent('Memory-');
expect(chart).toHaveTextContent('Disk-');
setResource({
...source,
memory: {
current: 0,
observation: {
state: 'current',
source: 'status-mem',
observedAt: '2026-10-04T13:00:00Z',
},
},
proxmox: { diskStatusReason: '' },
});
expect(view.container.querySelector('[data-history-group="utilization"]')).toBe(chart);
expect(chart.querySelector('[data-history-current="memory"]')).toHaveTextContent(
'0.0%current',
);
expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent(
'50.0%current',
);
expect(chart.querySelector('[data-history-deferred]')).toBeNull();
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');
@@ -2,10 +2,12 @@ import { describe, expect, it } from 'vitest';
import {
getResourceMetricsHistoryCurrentMetrics,
getResourceMetricsHistoryDeferredMetrics,
getResourceMetricsHistoryGroups,
getResourceMetricsHistoryTarget,
} from '@/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel';
import type { MetricsHistoryTargetResourceType, Resource } from '@/types/resource';
import { guestDiskDeferrals } from '@/components/Workloads/__fixtures__/guestDiskDeferrals';
// Minimal valid Resource fixture; mirrors the baseResource() convention used by
// sibling branchcov tests in this directory.
@@ -22,6 +24,149 @@ const baseResource = (overrides: Partial<Resource>): Resource => ({
...overrides,
});
describe('canonical drawer History honours selected guest observation provenance', () => {
const resource = (
state: string,
value = 25,
observedAt: string | undefined = '2026-10-04T12:00:00Z',
) =>
baseResource({
type: 'vm',
platformType: 'proxmox-pve',
memory: { current: value, observation: { state, source: 'guest-agent-meminfo', observedAt } },
disk: { current: 50 },
cpu: { current: 10 },
network: { rxBytes: 0, txBytes: 200 },
proxmox: { diskStatusReason: 'prev-vm-locked' },
});
it.each(['vm', 'system-container'] as const)(
'retains %s memory with its original time, not Last seen',
(type) => {
const value = {
...resource('last-known'),
type,
lastSeen: Date.parse('2026-10-05T00:00:00Z'),
};
expect(getResourceMetricsHistoryCurrentMetrics(value)).toMatchObject({
memory: undefined,
cpu: 10,
netin: 0,
});
expect(getResourceMetricsHistoryDeferredMetrics(value).memory).toMatchObject({
lastKnownValue: 25,
message: expect.stringContaining('2026-10-04 12:00:00 UTC'),
});
expect(getResourceMetricsHistoryDeferredMetrics(value).memory.message).not.toContain(
'2026-10-05',
);
// Container disk usage is not a QEMU guest-agent filesystem read.
expect(getResourceMetricsHistoryCurrentMetrics(value).disk).toBe(
type === 'vm' ? undefined : 50,
);
},
);
it.each([-1, 101, NaN, Infinity])('withdraws invalid retained memory %s', (value) => {
expect(
getResourceMetricsHistoryDeferredMetrics(resource('last-known', value)).memory.lastKnownValue,
).toBeUndefined();
expect(
getResourceMetricsHistoryCurrentMetrics(resource('last-known', value)).memory,
).toBeUndefined();
});
it.each([undefined, 'invalid', '0001-01-01T00:00:00Z', '2999-01-01T00:00:00Z'])(
'does not promote unusable current timestamps %s',
(observedAt) => {
const value = resource('current');
value.memory!.observation!.observedAt = observedAt;
expect(getResourceMetricsHistoryCurrentMetrics(value).memory).toBeUndefined();
expect(getResourceMetricsHistoryDeferredMetrics(value).memory).toMatchObject({
lastKnownValue: 25,
valueLabel: 'freshness unknown',
});
expect(getResourceMetricsHistoryDeferredMetrics(value).memory.message).toContain(
'Observation time unknown.',
);
expect(getResourceMetricsHistoryDeferredMetrics(value).memory.message).not.toMatch(
/invalid|2999|0001/,
);
},
);
it('keeps unavailable, true zero and independent current memory distinct from disk deferral', () => {
expect(
getResourceMetricsHistoryDeferredMetrics(resource('unavailable', 0)).memory.lastKnownValue,
).toBeUndefined();
expect(
getResourceMetricsHistoryDeferredMetrics(resource('last-known', 0)).memory.lastKnownValue,
).toBe(0);
expect(getResourceMetricsHistoryCurrentMetrics(resource('current', 0)).memory).toBe(0);
expect(getResourceMetricsHistoryDeferredMetrics(resource('current', 0)).memory).toBeUndefined();
expect(
getResourceMetricsHistoryDeferredMetrics(resource('current', 0)).disk.lastKnownValue,
).toBe(50);
});
it('qualifies unannotated Proxmox memory but leaves unrelated platforms unchanged', () => {
const value = resource('current');
delete value.memory!.observation;
expect(getResourceMetricsHistoryCurrentMetrics(value).memory).toBeUndefined();
expect(getResourceMetricsHistoryDeferredMetrics(value).memory.valueLabel).toBe(
'freshness unknown',
);
const vsphere = { ...value, platformType: 'vmware-vsphere' as const };
expect(getResourceMetricsHistoryCurrentMetrics(vsphere)).toMatchObject({
memory: 25,
disk: 50,
});
expect(getResourceMetricsHistoryDeferredMetrics(vsphere)).toEqual({});
const scoped = { ...vsphere, platformScopes: ['proxmox-pve'] };
expect(getResourceMetricsHistoryCurrentMetrics(scoped).memory).toBeUndefined();
const agent = {
...resource('last-known'),
type: 'agent' as const,
platformType: 'agent' as const,
};
expect(getResourceMetricsHistoryDeferredMetrics(agent).memory.lastKnownValue).toBe(25);
expect(getResourceMetricsHistoryDeferredMetrics(agent).disk).toBeUndefined();
});
it('does not echo unrecognised source or state text', () => {
const value = resource('private raw state');
value.memory!.observation!.source = 'private raw source';
expect(getResourceMetricsHistoryDeferredMetrics(value).memory).toMatchObject({
valueLabel: 'freshness unknown',
message: expect.stringContaining('Unknown source'),
});
expect(getResourceMetricsHistoryDeferredMetrics(value).memory.message).not.toContain(
'private raw',
);
});
it.each(guestDiskDeferrals)(
'qualifies both retained and unavailable filesystem %s readings',
(reason, message) => {
const value = resource('current');
value.proxmox!.diskStatusReason = `prev-${reason}`;
expect(getResourceMetricsHistoryCurrentMetrics(value).disk).toBeUndefined();
expect(getResourceMetricsHistoryDeferredMetrics(value).disk).toEqual({
lastKnownValue: 50,
message: `Using last known disk stats. ${message}`,
});
value.proxmox!.diskStatusReason = reason;
expect(getResourceMetricsHistoryDeferredMetrics(value).disk).toEqual({
lastKnownValue: undefined,
message,
});
value.proxmox!.diskStatusReason = '';
expect(getResourceMetricsHistoryDeferredMetrics(value).disk).toBeUndefined();
expect(getResourceMetricsHistoryCurrentMetrics(value).disk).toBe(50);
},
);
});
describe('getResourceMetricsHistoryTarget branch coverage', () => {
it('returns the metricsTarget pair when both resourceType and resourceId resolve', () => {
// TRUE arm of `if (metricsType && metricsId)`.
@@ -3,7 +3,10 @@ import {
GPU_METRICS_HISTORY_GROUPS,
HOST_METRICS_HISTORY_GROUPS,
} from '@/components/shared/hostMetricsHistoryModel';
import type { GuestDrawerHistoryTarget } from '@/components/Workloads/guestDrawerModel';
import type {
GuestDrawerHistoryDeferredMetric,
GuestDrawerHistoryTarget,
} from '@/components/Workloads/guestDrawerModel';
import {
GUEST_DRAWER_HISTORY_GROUPS,
type GuestDrawerHistoryGroupConfig,
@@ -12,6 +15,8 @@ import type { HostGPUSensor } from '@/types/api';
import type { Resource } from '@/types/resource';
import { getDiskPercent, getMemoryPercent } from '@/types/resource';
import { asTrimmedString } from '@/utils/stringUtils';
import { getMemoryObservationPresentation } from '@/utils/memoryObservation';
import { getWorkloadGuestDiskStatusMessage } from '@/utils/workloadGuestPresentation';
const finiteMetric = (value: number | undefined): number | undefined =>
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
@@ -152,15 +157,62 @@ export const getResourceMetricsHistoryTarget = (
export const resourceSupportsMetricsHistory = (resource: Resource): boolean =>
getResourceMetricsHistoryTarget(resource) !== null;
const isProxmoxGuest = (resource: Resource): boolean =>
(resource.type === 'vm' || resource.type === 'system-container') &&
(resource.platformType === 'proxmox-pve' ||
resource.platformScopes?.includes('proxmox-pve') === true);
const memoryReading = (resource: Resource) =>
getMemoryObservationPresentation(
resource.memory ? finiteMetric(getMemoryPercent(resource)) : undefined,
resource.memory?.observation,
isProxmoxGuest(resource),
);
const diskReadReason = (resource: Resource): string | undefined =>
resource.type === 'vm' && isProxmoxGuest(resource)
? resource.proxmox?.diskStatusReason || undefined
: undefined;
export const getResourceMetricsHistoryDeferredMetrics = (
resource: Resource,
): Record<string, GuestDrawerHistoryDeferredMetric> => {
const metrics: Record<string, GuestDrawerHistoryDeferredMetric> = {};
const memory = memoryReading(resource);
if (memory && memory.state !== 'current') {
metrics.memory = {
lastKnownValue:
memory.state === 'unavailable' ? undefined : finiteMetric(getMemoryPercent(resource)),
...(memory.state === 'unknown' ? { valueLabel: 'freshness unknown' as const } : {}),
message: memory.message,
};
}
const reason = diskReadReason(resource);
if (reason) {
metrics.disk = {
lastKnownValue:
reason.startsWith('prev-') && resource.disk
? finiteMetric(getDiskPercent(resource))
: undefined,
message: getWorkloadGuestDiskStatusMessage(reason),
};
}
return metrics;
};
export const getResourceMetricsHistoryCurrentMetrics = (
resource: Resource,
): Record<string, number | undefined> => {
const diskPercent = resource.disk ? finiteMetric(getDiskPercent(resource)) : undefined;
const temperature = finiteMetric(resource.temperature);
const memory = memoryReading(resource);
return {
cpu: finiteMetric(resource.cpu?.current),
memory: resource.memory ? finiteMetric(getMemoryPercent(resource)) : undefined,
disk: diskPercent,
memory:
resource.memory && (!memory || memory.state === 'current')
? finiteMetric(getMemoryPercent(resource))
: undefined,
disk: diskReadReason(resource) ? undefined : diskPercent,
usage: diskPercent,
netin: finiteMetric(resource.network?.rxBytes),
netout: finiteMetric(resource.network?.txBytes),
@@ -63,6 +63,7 @@ import {
} from './resourceDetailDrawerOperationalModel';
import {
getResourceMetricsHistoryCurrentMetrics,
getResourceMetricsHistoryDeferredMetrics,
getResourceMetricsHistoryGroups,
getResourceMetricsHistoryTarget,
resourceSupportsMetricsHistory,
@@ -371,6 +372,9 @@ export const useResourceDetailDrawerDerivedState = (
const metricsHistoryCurrentMetrics = createMemo(() =>
getResourceMetricsHistoryCurrentMetrics(resource()),
);
const metricsHistoryDeferredMetrics = createMemo(() =>
getResourceMetricsHistoryDeferredMetrics(resource()),
);
const metricsHistoryGroups = createMemo(() => getResourceMetricsHistoryGroups(resource()));
const hasMetricsHistory = createMemo(() => resourceSupportsMetricsHistory(resource()));
const hasDiscoveryTab = createMemo(
@@ -509,6 +513,7 @@ export const useResourceDetailDrawerDerivedState = (
serviceDetailsSummary,
metricsHistoryTarget,
metricsHistoryCurrentMetrics,
metricsHistoryDeferredMetrics,
metricsHistoryGroups,
hasMetricsHistory,
hasDiscoveryTab,
@@ -220,6 +220,57 @@ afterEach(() => {
// ── Tests ──────────────────────────────────────────────────────────────
describe('GuestDrawer', () => {
it('keeps the shared memory provenance policy in the full guest drawer after live replacement', async () => {
chartsApiMocks.getMetricsHistory.mockResolvedValue({
resourceType: 'vm',
resourceId: 'inst1:node1:100',
range: '24h',
start: 1,
end: 3,
source: 'store',
metrics: {},
});
const [guest, setGuest] = createSignal(
makeGuest({
memory: {
total: 100,
used: 0,
free: 100,
usage: 0,
observation: {
state: 'last-known',
source: 'guest-agent-meminfo',
observedAt: '2026-10-04T12:00:00Z',
},
},
}),
);
render(() => <GuestDrawer guest={guest()} onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('tab', { name: 'History' }));
await waitFor(() => expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalledTimes(1));
const chart = screen.getAllByTestId('guest-history-group-chart')[0];
expect(chart.querySelector('[data-history-last-known="memory"]')).toHaveTextContent(
'0.0%last known',
);
expect(chart.querySelector('[data-history-current="memory"]')).toBeNull();
setGuest({ ...guest(), lastSeen: '2026-10-05T00:00:00Z' });
expect(chart.querySelector('[data-history-deferred="memory"]')).toHaveTextContent(
'2026-10-04 12:00:00 UTC',
);
setGuest({
...guest(),
memory: {
...guest().memory,
observation: { state: 'current', source: 'status-mem', observedAt: '2026-10-04T13:00:00Z' },
},
});
expect(screen.getAllByTestId('guest-history-group-chart')[0]).toBe(chart);
expect(chart.querySelector('[data-history-current="memory"]')).toHaveTextContent('0.0%current');
expect(chart.querySelector('[data-history-deferred="memory"]')).toBeNull();
expect(chart.querySelector('path')).toBeNull();
expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalledTimes(1);
});
it('keeps stored History gaps through live guest recovery without another read', async () => {
const points = makeHistoryPoints(10);
const payload = {
@@ -17,6 +17,11 @@ import { getWorkloadMetricHistoryTarget } from '@/utils/workloadMetricHistoryTar
import type { NestedWorkloadContext } from './nestedWorkloadContext';
import type { WorkloadsMemoryDisplayBasis } from './workloadsFilterModel';
import {
getMemoryObservationPresentation,
type MemoryObservationPresentation,
} from '@/utils/memoryObservation';
type Guest = WorkloadGuest;
export interface GuestDrawerProps {
@@ -107,24 +112,7 @@ const getGuestDrawerDiskUsage = (guest: Guest): number | undefined => {
return typeof usage === 'number' && Number.isFinite(usage) && usage >= 0 ? usage : undefined;
};
export interface GuestDrawerMemoryReading {
state: 'current' | 'last-known' | 'unavailable' | 'unknown';
summary: string;
message: string;
}
const memorySourceLabels: Record<string, string> = {
'guest-agent-meminfo': 'QEMU guest agent',
'guest-agent-meminfo-derived': 'QEMU guest agent',
agent: 'Pulse Agent',
'available-field': 'Proxmox',
'derived-free-buffers-cached': 'Proxmox',
'derived-total-minus-used': 'Proxmox',
'status-mem': 'Proxmox',
'status-freemem': 'Proxmox',
'cluster-resources': 'Proxmox',
'previous-snapshot': 'Previous snapshot',
};
export type GuestDrawerMemoryReading = MemoryObservationPresentation;
const getGuestDrawerMemoryUsage = (guest: Guest): number | undefined => {
const usage = guest.memory?.usage;
@@ -149,41 +137,11 @@ export const getGuestDrawerMemoryReading = (guest: Guest): GuestDrawerMemoryRead
guest.type === 'lxc' ||
guest.platformScopes?.includes('proxmox-pve') ||
(guest.vmid > 0 && Boolean(guest.node && guest.instance)));
// Unannotated unrelated platforms retain their existing behaviour. Legacy
// Proxmox readings lack authority to assert current freshness.
if (!proxmoxGuest && !observation) return null;
const timestamp =
typeof observation?.observedAt === 'string' ? Date.parse(observation.observedAt) : NaN;
const validTime = Number.isFinite(timestamp) && timestamp > 0 && timestamp <= Date.now();
const observed = validTime
? new Date(timestamp).toISOString().replace('T', ' ').replace('.000Z', 'Z').replace('Z', ' UTC')
: null;
const state =
getGuestDrawerMemoryUsage(guest) === undefined || observation?.state === 'unavailable'
? 'unavailable'
: observation?.state === 'last-known'
? 'last-known'
: observation?.state === 'current' && validTime
? 'current'
: 'unknown';
const label =
state === 'current'
? 'Current'
: state === 'last-known'
? 'Last known'
: state === 'unavailable'
? 'Unavailable'
: 'Freshness unknown';
const source =
observation && Object.hasOwn(memorySourceLabels, observation.source)
? memorySourceLabels[observation.source]
: 'Unknown source';
const time = observed ? `Observed: ${observed}.` : 'Observation time unknown.';
return {
state,
summary: `${label} · ${source}${observed ? ` · ${observed}` : ' · time unknown'}`,
message: `${label}. Source: ${source}. ${time}${state === 'last-known' || state === 'unknown' ? ' Not a current measurement.' : ''}`,
};
return getMemoryObservationPresentation(
getGuestDrawerMemoryUsage(guest),
observation,
proxmoxGuest,
);
};
export const getGuestDrawerDeferredMetrics = (
@@ -11,3 +11,70 @@ export const readMemoryObservation = (value: unknown): MemoryObservation | undef
...(typeof record.observedAt === 'string' ? { observedAt: record.observedAt } : {}),
};
};
export interface MemoryObservationPresentation {
state: 'current' | 'last-known' | 'unavailable' | 'unknown';
summary: string;
message: string;
}
const memorySourceLabels: Record<string, string> = {
'guest-agent-meminfo': 'QEMU guest agent',
'guest-agent-meminfo-derived': 'QEMU guest agent',
agent: 'Pulse Agent',
'available-field': 'Proxmox',
'derived-free-buffers-cached': 'Proxmox',
'derived-total-minus-used': 'Proxmox',
'status-mem': 'Proxmox',
'status-freemem': 'Proxmox',
'cluster-resources': 'Proxmox',
'previous-snapshot': 'Previous snapshot',
};
// Both guest and canonical resource drawers must honour the original observation,
// not a snapshot refresh time or another metric's read state.
export const getMemoryObservationPresentation = (
value: number | undefined,
observation: MemoryObservation | undefined,
requiresObservation = false,
): MemoryObservationPresentation | null => {
// Unannotated unrelated platforms retain their existing behaviour. Legacy
// Proxmox readings lack authority to assert current freshness.
if (!requiresObservation && !observation) return null;
const timestamp =
typeof observation?.observedAt === 'string' ? Date.parse(observation.observedAt) : NaN;
const validTime = Number.isFinite(timestamp) && timestamp > 0 && timestamp <= Date.now();
const observed = validTime
? new Date(timestamp).toISOString().replace('T', ' ').replace('.000Z', 'Z').replace('Z', ' UTC')
: null;
const state =
typeof value !== 'number' ||
!Number.isFinite(value) ||
value < 0 ||
value > 100 ||
observation?.state === 'unavailable'
? 'unavailable'
: observation?.state === 'last-known'
? 'last-known'
: observation?.state === 'current' && validTime
? 'current'
: 'unknown';
const label =
state === 'current'
? 'Current'
: state === 'last-known'
? 'Last known'
: state === 'unavailable'
? 'Unavailable'
: 'Freshness unknown';
const source =
observation && Object.hasOwn(memorySourceLabels, observation.source)
? memorySourceLabels[observation.source]
: 'Unknown source';
const time = observed ? `Observed: ${observed}.` : 'Observation time unknown.';
return {
state,
summary: `${label} · ${source}${observed ? ` · ${observed}` : ' · time unknown'}`,
message: `${label}. Source: ${source}. ${time}${state === 'last-known' || state === 'unknown' ? ' Not a current measurement.' : ''}`,
};
};