Keep deferred filesystem detail values distinct from current health

Label retained guest usage beside each mount and remove current utilization bars and threshold colours until a fresh read. Do not promote numeric carriers on failed or explicitly unavailable reads; retain identity and known capacity, current/LXC controls and the existing safe read explanation. Production drawer browser proof, mounted regressions and pure builder controls preserve the local request budget.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-04 22:48:00 +01:00
parent 664b29f930
commit c37dec157e
9 changed files with 303 additions and 10 deletions
@@ -249,6 +249,24 @@ Fresh linked Pulse-agent disk evidence with no reason remains unmarked, and
non-VM rows do not inherit QGA guidance. The cue adds no action or tab stop;
touch on it continues through the existing row disclosure to the full reason.
The opened guest Overview applies the same admission to every filesystem, not
only the aggregate row or its status sentence. A valid `prev-` value is labelled
**Last known** beside its mount identity and used/capacity bytes; retained values
do not receive current utilization bars or threshold colours. Other VM read
reasons and explicit disk telemetry unavailability show **Usage unavailable**,
retaining identity and known capacity without promoting old used bytes. Missing
or invalid retained usage is unavailable, not an empty healthy filesystem. The
read explanation precedes the filesystem breakdown and remains visible before
any sample. Clearing a lock alone cannot clear a still-supplied reason. A fresh
same-guest read restores the normal accessible utilization bar; independent LXC
and linked-agent readings with no VM reason retain their existing semantics.
Verification: `GuestDrawerOverview.filesystem-provenance.test.tsx`, the existing
filesystem regressions and builder controls in `DiskList.test.tsx`, plus
`browser-tests/guest-filesystem-provenance.cjs` using the full production guest
drawer on desktop/light/dark and touch WebKit. These are synthetic presentation
and input proofs, not native backup/thaw, covered-filesystem writes or resumption.
Verification: mounted `GuestRow.test.tsx` and `MetricMiniSparkline.test.tsx`
regressions and `browser-tests/guest-disk-provenance.cjs` exercise retained and
pre-sample states, display/lens changes, current-value semantics and same-row
@@ -1671,6 +1671,15 @@ without contention or cross-delivery.
lifecycle. Request handlers may reuse the cached manager but must not reopen
or remigrate SQLite on each role or assignment request, and shutdown must
close the owned manager set.
8. Guest Overview filesystem provenance is local presentation over the already
supplied snapshot: no additional query, timer, poll, History fetch or guest
command may be added to establish it. Its existing linear filesystem mapping
may label retained values and omit current utilization bars; it must preserve
ordinary current-value builder behaviour and must not mutate the snapshot.
`DiskList.test.tsx` verifies the pure builder's retained/unavailable/current
controls; mounted drawer regressions and the full-drawer browser proof cover
the shipped path. No performance or native workload improvement follows from
the presentation proof alone.
## Current State
@@ -0,0 +1,136 @@
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/filesystem-read-provenance/browser-final';
const origin = 'http://127.0.0.1:5324';
const runtime = [
'frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx',
'frontend-modern/src/components/Workloads/diskListModel.ts',
];
const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const settle = page => page.evaluate(async () => {
await document.fonts.ready;
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
(async () => {
fs.mkdirSync(output, { recursive: true });
const result = {
result: 'incomplete', playwright: require('playwright/package.json').version,
runtime_hashes: Object.fromEntries(runtime.map(file => [file, hash('/workspace/' + file)])),
cases: [], screenshots: [], cleanup: { browser: false, server: false },
limits: 'Real GuestRow/full GuestDrawer/DetailSectionTable and CSS; synthetic guest observations and APIs. No native QGA, backup, cause, thaw, writes, 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', server: { host: '127.0.0.1', port: 5324, strictPort: true }});
let browser;
try {
await server.listen();
for (const [name, engine, width, dark] of [
['chromium-desktop-light', chromium, 1365, false],
['chromium-desktop-dark', chromium, 1365, true],
['webkit-phone-dark', webkit, 390, true],
['webkit-narrow-light', webkit, 320, false],
]) {
browser = await engine.launch(engine === chromium ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } : { headless: true });
const context = await browser.newContext({viewport: { width, height: width <= 768 ? 844 : 900 }, isMobile: width <= 768, hasTouch: width <= 768, locale: 'en-GB', timezoneId: 'UTC'});
const page = await context.newPage();
page.setDefaultTimeout(15000);
page.setDefaultNavigationTimeout(60000);
const record = {name, browser: browser.version(), width, dark, errors: [], requests: [], checks: [], states: []};
result.cases.push(record);
page.on('pageerror', error => record.errors.push(error.message));
await page.route('**/*', route => {
const request = route.request(); const url = new URL(request.url());
if (url.origin !== origin) { record.requests.push({ method: request.method(), path: url.pathname, disposition: 'off-origin blocked' }); return route.abort(); }
if (url.pathname.startsWith('/api/')) {
record.requests.push({ method: request.method(), path: url.pathname, disposition: 'synthetic' });
return route.fulfill({status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: [], alerts: [], anomalies: [], config: null })});
}
return route.continue();
});
await page.addInitScript(isDark => {
document.addEventListener('DOMContentLoaded', () => { if (isDark) document.documentElement.classList.add('dark'); });
window.__trustedInput = [];
document.addEventListener('click', event => window.__trustedInput.push({ type: 'click', trusted: event.isTrusted }), true);
document.addEventListener('keydown', event => window.__trustedInput.push({ type: event.key, trusted: event.isTrusted }), true);
}, dark);
await page.goto(`${origin}/browser-tests/guest-disk-provenance.html`);
await page.waitForFunction(() => window.__guestDiskProvenance);
const apply = async next => { await page.evaluate(next => window.__guestDiskProvenance.apply(next), next); await settle(page); };
await apply({ reason: 'prev-vm-locked', data: true, usage: 90, lock: 'backup', kind: 'qemu' });
const row = page.locator('[data-guest-id="fixture-pve:pve-a:101"]');
const disclosure = row.locator('[data-row-action]');
if (width <= 768) await row.locator('[data-workload-disk-read-status]').tap();
else { await disclosure.focus(); await page.keyboard.press('Enter'); }
const drawer = page.getByRole('region', {name: 'Guest details'});
await drawer.waitFor({state: 'visible'});
const technical = drawer.getByTestId('guest-technical-details');
const filesystem = technical.getByText('/data', {exact: true}).locator('xpath=ancestor::tr');
const progress = filesystem.getByRole('progressbar');
const text = async () => (await filesystem.innerText()).replace(/\s+/g, ' ').trim();
const capture = async state => {
await technical.scrollIntoViewIfNeeded(); await settle(page);
const file = `${name}-${state}.png`;
await technical.screenshot({path: path.join(output, file)});
result.screenshots.push({file, sha256: hash(path.join(output, file)), name, state});
};
assert.match(await text(), /Last known 90% · 9\.00 GB\/10\.0 GB · EXT4/);
assert.equal(await progress.count(), 0);
record.checks.push('Retained quantitative values are labelled last known beside their filesystem identity, with no current utilization bar.');
record.states.push({state: 'retained', text: await text()});
await capture('retained');
await apply({ lock: '' });
assert.match(await text(), /Last known 90%/); assert.equal(await progress.count(), 0);
record.checks.push('Clearing only the operation lock cannot promote a deferred reading.');
const reasons = await page.evaluate(() => window.__guestDiskProvenance.deferrals.map(item => item[0]));
for (const reason of reasons) {
await apply({reason: 'prev-' + reason});
assert.match(await text(), /Last known 90%/); assert.equal(await progress.count(), 0);
await apply({reason});
assert.match(await text(), /Usage unavailable · \?\/10\.0 GB · EXT4/);
assert.doesNotMatch(await text(), /90%|9\.00 GB/); assert.equal(await progress.count(), 0);
}
record.checks.push('All eight fixed read reasons distinguish retained data from unavailable numeric carriers.');
record.states.push({state: 'unavailable', text: await text()});
await capture('unavailable');
await apply({reason: '', usage: 30, lock: ''});
assert.match(await text(), /30% · 3\.00 GB\/10\.0 GB · EXT4/);
assert.equal(await progress.getAttribute('aria-valuenow'), '30');
assert.equal(await progress.getAttribute('aria-label'), 'Filesystem /data utilization');
assert.equal(await drawer.isVisible(), true);
record.checks.push('A same-guest fresh read restores the correct numeric value and accessible utilization bar without closing the drawer.');
record.states.push({state: 'fresh', text: await text()});
await capture('fresh');
await apply({reason: 'prev-vm-locked', kind: 'lxc'});
assert.match(await text(), /30% · 3\.00 GB\/10\.0 GB · EXT4/); assert.equal(await progress.getAttribute('aria-valuenow'), '30');
record.checks.push('Independent LXC filesystem evidence does not inherit a VM-only reason.');
await apply({kind: 'qemu', data: false, reason: 'vm-locked'});
assert.equal(await technical.getByText('/data', {exact: true}).count(), 0);
assert.match(await technical.innerText(), /Guest reads paused while Proxmox reports a VM operation lock/);
record.checks.push('With no prior filesystem sample, the readable explanation remains and no filesystem value is invented.');
const overflow = await technical.evaluate(el => ({width: el.clientWidth, scrollWidth: el.scrollWidth, documentWidth: document.documentElement.clientWidth, documentScroll: document.documentElement.scrollWidth}));
assert.ok(overflow.scrollWidth <= overflow.width + 1, JSON.stringify(overflow));
record.layout = overflow;
await drawer.getByRole('button', {name: 'Collapse backup-guest details'}).click();
assert.equal(await drawer.count(), 0);
record.input = await page.evaluate(() => window.__trustedInput);
assert.ok(record.input.some(event => event.trusted));
assert.deepEqual(record.errors, []);
assert.equal(record.requests.filter(request => !['GET','HEAD'].includes(request.method)).length, 0);
record.checks.push('Trusted keyboard/touch disclosure and close work; no uncaught page error, overflow or mutating request.');
await context.close(); await browser.close(); browser = null;
}
result.result = 'passed';
} catch (error) {
result.error = String(error.stack || error); throw error;
} finally {
if (browser) await browser.close(); result.cleanup.browser = 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,60 @@
{
"version": 1,
"result": "passed",
"base_sha": "664b29f930b19f91b74f673914c85e3b806b54f7",
"changed_paths": [
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
"frontend-modern/src/components/Workloads/diskListModel.ts"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "16ccfd4c8a865c02d4ab3580d7872518cb599715245fb10ebbe3dfbe375cb104",
"frontend-modern/src/components/Workloads/diskListModel.ts": "8b43eeb3e08dff191039ee19ea74baa89703d6b8f89d801a5ff0f8cdef09dffc"
},
"routes": [
"/browser-tests/guest-disk-provenance.html \u2014 production Workloads GuestRow/full GuestDrawer; synthetic APIs, not the full App route"
],
"states": [
"Retained filesystem usage with each of eight prev- reasons: labelled last known, no current bars/threshold colours",
"Same eight non-retained read failures: unavailable used bytes, identity/capacity retained",
"Lock-only clearance retains the label; same-guest fresh read restores accessible 30% utilization",
"LXC independent reading and no-sample readable VM status"
],
"interactions": [
"Trusted desktop Enter disclosure and close",
"Trusted WebKit phone/narrow touch on the row provenance cue and close",
"Synthetic same-guest poll replacements while keeping the drawer open",
"Inspected every retained/unavailable/fresh screenshot in all four browser/theme/width groups"
],
"viewports": [
{
"width": 1365,
"height": 900,
"browser": "chromium-desktop-light",
"theme": "light"
},
{
"width": 1365,
"height": 900,
"browser": "chromium-desktop-dark",
"theme": "dark"
},
{
"width": 390,
"height": 844,
"browser": "webkit-phone-dark",
"theme": "dark"
},
{
"width": 320,
"height": 844,
"browser": "webkit-narrow-light",
"theme": "light"
}
],
"verified_at": "2026-10-04T21:47:04Z",
"playwright_version": "1.56.1",
"checks": 28,
"screenshots_inspected": 12,
"script": "frontend-modern/browser-tests/guest-filesystem-provenance.cjs",
"limits": "Real GuestRow/full GuestDrawer/DetailSectionTable and CSS; synthetic guest observations and APIs. No native QGA, backup, cause, thaw, writes, installed/reporter or release acceptance."
}
@@ -138,12 +138,22 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) {
props.guest.vmware?.datacenterName ||
props.guest.vmware?.clusterName,
);
const diskReason = isGuestDrawerVM(props.guest) ? props.guest.diskStatusReason : undefined;
const diskReadState =
props.guest.telemetryAvailability?.disk === false
? 'unavailable'
: diskReason
? diskReason.startsWith('prev-')
? 'last-known'
: 'unavailable'
: 'current';
const diskRows = (props.guest.disks ?? []).map((disk, index) =>
buildWorkloadsDiskPresentation(disk, index, props.diskThresholds),
buildWorkloadsDiskPresentation(disk, index, props.diskThresholds, diskReadState),
);
const diskStatusMessage =
isGuestDrawerVM(props.guest) && props.guest.diskStatusReason
? getWorkloadGuestDiskStatusMessage(props.guest.diskStatusReason)
const diskStatusMessage = diskReason
? getWorkloadGuestDiskStatusMessage(diskReason)
: diskReadState === 'unavailable'
? 'Filesystem usage is unavailable.'
: null;
return compactDetailSections([
@@ -217,6 +227,7 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) {
? {
label: 'Filesystems',
rows: compactDetailRows([
makeDetailRow('Status', diskStatusMessage, { layout: 'stacked', wrap: true }),
...diskRows.map((disk) =>
makeDetailRow(
disk.label,
@@ -238,7 +249,6 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) {
},
),
),
makeDetailRow('Status', diskStatusMessage, { layout: 'stacked', wrap: true }),
]),
}
: null,
@@ -28,6 +28,38 @@ afterEach(() => {
cleanup();
});
describe('filesystem presentation read admission', () => {
it.each(['last-known', 'unavailable'] as const)(
'keeps %s readings out of threshold-coloured utilization',
(state) => {
const disk = makeDisk();
const reading = buildWorkloadsDiskPresentation(disk, 0, undefined, state);
expect(reading.progressValue).toBeNull();
expect(reading.progressWidth).toBe('0%');
expect(reading.progressClass).toBe('bg-surface-hover');
expect(reading.label).toBe('/');
expect(reading.typeLabel).toBe('EXT4');
expect(reading.usagePercentLabel).toBe(
state === 'last-known' ? 'Last known 50%' : 'Usage unavailable',
);
expect(reading.usageText).toBe(state === 'last-known' ? '50.0 GB/100 GB' : '?/100 GB');
expect(disk.used).toBe(53687091200);
},
);
it('does not infer a retained value from capacity or free bytes', () => {
const reading = buildWorkloadsDiskPresentation(
makeDisk({ used: undefined }),
0,
undefined,
'last-known',
);
expect(reading.usagePercentLabel).toBe('Usage unavailable');
expect(reading.usageText).toBe('?/100 GB');
expect(reading.progressValue).toBeNull();
});
});
describe('DiskList', () => {
describe('fallback when no disks', () => {
it('renders fallback "-" when disks array is empty', () => {
@@ -111,6 +111,18 @@ describe('filesystem details do not promote retained usage to current health', (
expect(within(filesystemRow()).queryByRole('progressbar')).not.toBeInTheDocument();
});
it('explains explicit unavailability even before any filesystem sample', () => {
render(() => (
<GuestDrawerOverview
{...props(guest({ disks: [], telemetryAvailability: { disk: false } }))}
hasFilesystemDetails={false}
/>
));
expect(screen.getByText('Filesystems')).toBeVisible();
expect(screen.getByText('Filesystem usage is unavailable.')).toBeVisible();
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
it('keeps retained values until the reason clears, then resumes the same drawer', () => {
const [value, setValue] = createSignal(
guest({ diskStatusReason: 'prev-vm-locked', lock: 'backup', backupInProgress: true }),
@@ -85,6 +85,9 @@ describe('GuestDrawerOverview filesystem labels', () => {
).toHaveAttribute('colspan', '2');
expect(screen.queryByText('prev-vm-locked')).not.toBeInTheDocument();
for (const path of paths) expect(screen.getByText(path)).toBeInTheDocument();
expect(screen.getByText('Last known 50% · 5.00 GB/10.0 GB · MP0')).toBeVisible();
expect(screen.getByText('Usage unavailable · ?/10.0 GB · MP1')).toBeVisible();
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
setGuest({
...guest(),
diskStatusReason: '',
@@ -92,18 +92,31 @@ export const buildWorkloadsDiskPresentation = (
disk: Disk,
index: number,
thresholds?: MetricDisplayThresholds | null,
readState: 'current' | 'last-known' | 'unavailable' = 'current',
): WorkloadsDiskPresentation => {
const label = getWorkloadsDiskLabel(disk);
// Retention preserves identity and capacity, not a current utilization
// measurement. Only an explicitly retained read may display old used bytes.
const reading = readState === 'unavailable' ? { ...disk, used: undefined } : disk;
const percentLabel = getWorkloadsDiskUsagePercentLabel(reading);
return {
key: `${disk.mountpoint ?? ''}:${disk.device ?? ''}:${index}`,
label,
labelTitle: getWorkloadsDiskLabelTitle(label),
progressClass: getWorkloadsDiskProgressClass(disk, thresholds),
progressValue: getWorkloadsDiskProgressValue(disk),
progressWidth: getWorkloadsDiskProgressWidth(disk),
progressClass:
readState === 'current'
? getWorkloadsDiskProgressClass(reading, thresholds)
: 'bg-surface-hover',
progressValue: readState === 'current' ? getWorkloadsDiskProgressValue(reading) : null,
progressWidth: readState === 'current' ? getWorkloadsDiskProgressWidth(reading) : '0%',
typeLabel: getWorkloadsDiskTypeLabel(disk),
usageText: getWorkloadsDiskUsageText(disk),
usagePercentLabel: getWorkloadsDiskUsagePercentLabel(disk),
usageText: getWorkloadsDiskUsageText(reading),
usagePercentLabel:
readState === 'current'
? percentLabel
: percentLabel === '—'
? 'Usage unavailable'
: `Last known ${percentLabel}`,
};
};