From 8f09a5999617cb79a4e2b0a43b35de2f3b8f0d87 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 11:54:32 +0100
Subject: [PATCH 1/7] Keep failed PBS datastore reads out of healthy capacity
Preserve named unavailable and unusable datastore evidence without reporting healthy empty capacity. Keep measured zero, warning thresholds, independent backup inventory and row identity, with readable phone notices and connected contracts and controls.
Change-source: pulse-maintainer
---
.../subsystems/frontend-primitives.md | 17 ++
.../internal/subsystems/storage-recovery.md | 21 ++
.../internal/subsystems/unified-resources.md | 13 +
.../browser-tests/pbs-capacity.cjs | 228 ++++++++++++++++++
.../browser-tests/pbs-capacity.html | 12 +
.../browser-tests/pbs-capacity.tsx | 80 ++++++
.../proxmox/ProxmoxBackupServersTable.tsx | 123 +++++++---
...roxmoxBackupServersTable.capacity.test.tsx | 146 +++++++++++
.../ProxmoxBackupServersTable.drawer.test.tsx | 4 +-
.../ProxmoxBackupServersTable.test.ts | 19 ++
.../__tests__/ProxmoxBackupsTable.test.tsx | 46 ++++
.../proxmoxBackupsTablePresentation.test.ts | 5 +-
.../proxmoxBackupsTablePresentation.ts | 4 +-
frontend-modern/src/types/resource.ts | 1 +
14 files changed, 683 insertions(+), 36 deletions(-)
create mode 100644 frontend-modern/browser-tests/pbs-capacity.cjs
create mode 100644 frontend-modern/browser-tests/pbs-capacity.html
create mode 100644 frontend-modern/browser-tests/pbs-capacity.tsx
create mode 100644 frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index b43e1bb73f..23a1cfed3e 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -20,6 +20,23 @@
## Purpose
+### PBS capacity warnings stay truthful and readable
+
+The existing Backups server table distinguishes unavailable or unusable named
+datastore capacity from an explicitly measured empty store. Fixed **Unavailable**
+and **Unknown** notices replace unreliable percentages, bytes and deduplication
+figures without exposing a provider error body. Server reachability, independent
+backup inventory and drawer identity retain their own evidence and semantics.
+Measured zero and the existing high-capacity warning thresholds stay unchanged.
+
+At compact widths, the shared 40% server-identity anchor stays in place; the Used
+column receives 25% so the warning remains readable without horizontal overflow.
+The redundant connection dot yields to its textual status. Datastore hover text
+does not change disclosure or row identity. The mounted capacity, drawer and
+full-page Backups tests plus `browser-tests/pbs-capacity.cjs` cover replacement,
+independent inventory, keyboard/touch and 320px/390px/desktop rendering. Synthetic
+presentation proof is not native datastore, backup, guest-thaw or release proof.
+
### Backup age requires a usable completed timestamp
Existing workload indicators, age badges and drawer protection context distinguish
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 0ebf9d1c10..4da8d3d071 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -21,6 +21,27 @@
## Purpose
+### PBS datastore capacity is not inferred from failed-read zeros
+
+The existing Backups server table distinguishes a named datastore whose status
+read failed from an empty datastore and from a server without datastore data.
+Provider unavailable/error evidence or an unavailable server connection
+withdraws current capacity and deduplication figures, without dropping the row,
+changing its drawer identity or invalidating independently readable backup
+inventory. Unknown status, missing/nonfinite/negative capacity and invalid
+authoritative percentages cannot receive a healthy usage indication. Capacity
+requires a positive finite total and a finite nonnegative used observation;
+measured zero remains zero, and the existing 75/90-percent warnings and
+over-capacity danger remain intact. A subsequent valid same-identity snapshot
+restores presentation without a remount, timer or new read.
+
+Fixed notices do not expose provider error text. Mounted
+`ProxmoxBackupServersTable.capacity.test.tsx` covers these distinctions and
+direct/canonical replacement; `ProxmoxBackupServersTable.test.ts` preserves
+source evidence and independent datastore identity. The production-component
+browser fixture checks desktop and narrow phone layouts. These synthetic
+checks establish no native datastore repair, backup/thaw or release acceptance.
+
### TrueNAS connection probes do not settle recovery inventory
A successful settings probe reads system information only, on a separate client.
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index ce79bd7a83..b6ad600f88 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -23,6 +23,19 @@ and sort the complete canonical change table while startup and ingestion wait.
## Purpose
+### PBS capacity consumers preserve explicit read failure
+
+`ResourcePBSDatastore.error` describes existing provider read-failure evidence,
+not a new collection or command. PBS resource facets retain that evidence
+through canonical presentation. The Backups capacity consumer must not treat
+the provider's zero-valued unavailable datastore as a successful empty-store
+observation, nor show retained numbers as current when the connection is
+unavailable. It keeps row/source identity and independent inventory counts;
+fixed unavailable/unknown notices never disclose the error body. Valid capacity
+including measured zero restores the same mounted row. Verification uses
+`ProxmoxBackupServersTable.test.ts` plus the direct/canonical mounted capacity
+controls; native read, backup and published acceptance remain separate.
+
### History consumers do not accept a mismatched or malformed success body
The common metrics History client validates the issued selection and response
diff --git a/frontend-modern/browser-tests/pbs-capacity.cjs b/frontend-modern/browser-tests/pbs-capacity.cjs
new file mode 100644
index 0000000000..5ba31895d6
--- /dev/null
+++ b/frontend-modern/browser-tests/pbs-capacity.cjs
@@ -0,0 +1,228 @@
+process.env.RAYON_NUM_THREADS = '2';
+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/pbs-capacity/browser';
+const origin = 'http://127.0.0.1:5321';
+const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
+const runtime = [
+ 'src/features/proxmox/ProxmoxBackupServersTable.tsx',
+ 'src/features/proxmox/proxmoxBackupsTablePresentation.ts',
+ 'src/types/resource.ts',
+];
+
+(async () => {
+ fs.mkdirSync(output, { recursive: true });
+ process.chdir(root);
+ const clientVersion = require('playwright/package.json').version;
+ assert.equal(
+ clientVersion,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ cacheDir: path.join(output, 'vite-cache'),
+ optimizeDeps: { entries: ['browser-tests/pbs-capacity.html'] },
+ server: { host: '127.0.0.1', port: 5321, strictPort: true },
+ });
+ const results = [];
+ let browser;
+ try {
+ await server.listen();
+ for (const [name, engine, width] of [
+ ['chromium-desktop', chromium, 1365],
+ ['webkit-phone', webkit, 390],
+ ['webkit-narrow', webkit, 320],
+ ]) {
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ for (const transport of ['direct', 'canonical']) {
+ const phone = engine === webkit;
+ const page = await browser.newPage({
+ viewport: { width, height: phone ? 844 : 900 },
+ isMobile: phone,
+ hasTouch: phone,
+ colorScheme: phone ? 'dark' : 'light',
+ });
+ if (phone) await page.addInitScript(() => document.documentElement.classList.add('dark'));
+ page.setDefaultTimeout(20000);
+ await page.clock.setFixedTime(new Date('2026-10-04T12:00:00Z'));
+ const errors = [],
+ offOrigin = [],
+ requests = [],
+ checks = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.route('**/*', async (route) => {
+ const req = route.request(),
+ url = new URL(req.url());
+ if (url.origin !== origin) {
+ offOrigin.push(url.origin);
+ return route.abort();
+ }
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ requests.push({ path: url.pathname, method: req.method() });
+ const data =
+ url.pathname === '/api/backups/pve'
+ ? { backupTasks: [], storageBackups: [], guestSnapshots: [] }
+ : url.pathname === '/api/backups/pbs'
+ ? {
+ backups: [
+ {
+ id: 'pbs-point',
+ vmid: '100',
+ backupType: 'vm',
+ backupTime: '2026-10-04T11:00:00Z',
+ files: ['index.json.blob'],
+ instance: 'pbs-main',
+ datastore: 'main',
+ protected: false,
+ verified: true,
+ },
+ ],
+ }
+ : { postures: [] };
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ data }),
+ });
+ });
+ await page.goto(`${origin}/browser-tests/pbs-capacity.html?transport=${transport}`);
+ // Imported production theme stores can initialise after addInitScript.
+ // Apply the requested CSS context after the fixture is loaded too.
+ if (phone) await page.evaluate(() => document.documentElement.classList.add('dark'));
+ const table = page.locator('[data-proxmox-backups-table="servers"]');
+ await table.waitFor();
+ const head = await table.locator('th').allTextContents();
+ const usedIndex = head.indexOf('Used');
+ assert(usedIndex >= 0);
+ const cell = table.locator('tbody tr').first().locator('td').nth(usedIndex);
+ await cell.locator('text=40.0%').waitFor();
+ await cell.evaluate((node) => {
+ node.dataset.identityWitness = 'original';
+ });
+ const activate = async (state) => {
+ const control = page.getByRole('button', { name: state, exact: true });
+ if (phone) await control.tap();
+ else {
+ await control.focus();
+ await control.press('Enter');
+ }
+ };
+ const checkNotice = async (state, label) => {
+ await activate(state);
+ await cell.getByText(label, { exact: true }).waitFor();
+ const evidence = await cell.evaluate((node) => ({
+ text: node.textContent,
+ witness: node.dataset.identityWitness,
+ success: !!node.querySelector('.bg-emerald-500'),
+ title: node.querySelector('[title]')?.title,
+ }));
+ assert.equal(evidence.text, label);
+ assert.equal(evidence.witness, 'original');
+ assert.equal(evidence.success, false);
+ assert(evidence.title.includes('does not mean the datastore is empty'));
+ assert(!(await table.textContent()).includes('2.0×'));
+ assert(
+ !(await page.locator('body').innerHTML()).includes('PRIVATE_PROVIDER_ERROR_SENTINEL'),
+ );
+ checks.push({ state, evidence });
+ };
+ await checkNotice('Failure', 'Unavailable');
+ await page.screenshot({
+ path: path.join(output, `${name}-${transport}-unavailable.png`),
+ fullPage: true,
+ });
+ await checkNotice('Unknown', 'Unknown');
+ await checkNotice('Offline', 'Unavailable');
+ await checkNotice('Error', 'Unavailable');
+ await activate('Zero');
+ await cell.getByText('0.0%', { exact: true }).waitFor();
+ await activate('Full');
+ await cell.getByText('95.0%', { exact: true }).waitFor();
+ assert((await cell.locator('.text-red-600').count()) > 0);
+ checks.push({
+ state: 'valid zero / full',
+ mounted: await cell.getAttribute('data-identity-witness'),
+ });
+ await activate('No stores');
+ await cell.getByText('No datastore data').waitFor();
+ await activate('Healthy');
+ await cell.getByText('40.0%', { exact: true }).waitFor();
+ await activate('Failure');
+ await cell.getByText('Unavailable', { exact: true }).waitFor();
+ const geometry = await cell.evaluate((node) => {
+ const label = node.querySelector('span'),
+ range = document.createRange();
+ range.selectNodeContents(label);
+ const text = range.getBoundingClientRect(),
+ box = node.getBoundingClientRect();
+ return {
+ text: { x: text.x, right: text.right, width: text.width },
+ cell: { x: box.x, right: box.right, width: box.width },
+ pageWidth: document.documentElement.clientWidth,
+ scrollWidth: document.documentElement.scrollWidth,
+ };
+ });
+ assert(
+ geometry.text.x >= geometry.cell.x && geometry.text.right <= geometry.cell.right,
+ JSON.stringify({ name, geometry }),
+ );
+ assert(geometry.scrollWidth <= geometry.pageWidth + 1, JSON.stringify(geometry));
+ checks.push({ state: 'warning fits', geometry });
+ assert.equal(errors.length, 0, errors.join('\n'));
+ assert.equal(offOrigin.length, 0);
+ assert(requests.every((req) => req.method === 'GET'));
+ results.push({
+ name,
+ transport,
+ width,
+ browserVersion: browser.version(),
+ checks,
+ requests,
+ errors,
+ offOrigin,
+ });
+ await page.close();
+ }
+ await browser.close();
+ browser = undefined;
+ }
+ fs.writeFileSync(
+ path.join(output, 'result.json'),
+ JSON.stringify(
+ {
+ result: 'passed',
+ clientVersion,
+ results,
+ content_sha256: Object.fromEntries(
+ runtime.map((file) => [`frontend-modern/${file}`, sha256(path.join(root, file))]),
+ ),
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(JSON.stringify({ result: 'passed', contexts: results.length, output }));
+ } catch (error) {
+ fs.writeFileSync(
+ path.join(output, 'failure.json'),
+ JSON.stringify({ message: error.message, completedContexts: results }, null, 2),
+ );
+ throw error;
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error.stack);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/pbs-capacity.html b/frontend-modern/browser-tests/pbs-capacity.html
new file mode 100644
index 0000000000..8cdc08fe2a
--- /dev/null
+++ b/frontend-modern/browser-tests/pbs-capacity.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ PBS capacity evidence
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/pbs-capacity.tsx b/frontend-modern/browser-tests/pbs-capacity.tsx
new file mode 100644
index 0000000000..7dab6d9930
--- /dev/null
+++ b/frontend-modern/browser-tests/pbs-capacity.tsx
@@ -0,0 +1,80 @@
+// Production table/router/CSS and canonical resource adapter. Synthetic read
+// states only: no provider, native datastore, backup or thaw operation.
+import { createMemo, createSignal } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Route, Router } from '@solidjs/router';
+import type { Resource, ResourcePBSDatastore } from '../src/types/resource';
+import { canonicalizeRealtimeResource } from '../src/utils/resourceStateAdapters';
+import { ProxmoxBackupsTable } from '../src/features/proxmox/ProxmoxBackupsTable';
+import '../src/index.css';
+
+const states = ['Healthy', 'Failure', 'Unknown', 'Offline', 'Zero', 'Full', 'Error', 'No stores'];
+const normal = {
+ name: 'main',
+ total: 10000,
+ used: 4000,
+ available: 6000,
+ usagePercent: 40,
+ status: 'available',
+ deduplicationFactor: 2,
+};
+
+function Fixture() {
+ const [state, setState] = createSignal('Healthy');
+ const servers = createMemo((): Resource[] => {
+ let store: ResourcePBSDatastore = { ...normal };
+ if (state() === 'Failure')
+ store = { ...store, total: 0, used: 0, available: 0, usagePercent: 0, status: 'unavailable' };
+ if (state() === 'Unknown')
+ store = { ...store, total: 0, used: 0, available: 0, usagePercent: 0 };
+ if (state() === 'Zero') store = { ...store, used: 0, usagePercent: 0, available: 10000 };
+ if (state() === 'Full') store = { ...store, used: 9500, usagePercent: 95, available: 500 };
+ if (state() === 'Error') store = { ...store, error: 'PRIVATE_PROVIDER_ERROR_SENTINEL' };
+ const pbs = {
+ instanceId: 'pbs-main',
+ connectionHealth: state() === 'Offline' ? 'offline' : 'healthy',
+ version: '3.2.1',
+ datastores: state() === 'No stores' ? [] : [store],
+ };
+ const resource: Resource = {
+ id: 'pbs-main',
+ type: 'pbs',
+ name: 'pbs-main',
+ platformId: 'pbs-main',
+ platformType: 'proxmox-pbs',
+ sourceType: 'api',
+ status: 'online',
+ lastSeen: Date.parse('2026-10-04T12:00:00Z'),
+ cpu: { current: 12 },
+ memory: { current: 40, total: 10000, used: 4000 },
+ pbs,
+ };
+ if (new URLSearchParams(location.search).get('transport') === 'canonical') {
+ resource.pbs = undefined;
+ resource.platformData = { sources: ['pbs'], pbs };
+ return [canonicalizeRealtimeResource(resource)];
+ }
+ return [resource];
+ });
+ return (
+
+
+ {states.map((value) => (
+
+ ))}
+
+ } workloads={[]} servers={servers()} />
+
+ );
+}
+
+render(
+ () => (
+
+
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
index 44997db7e3..8cb2aa0be7 100644
--- a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
+++ b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
@@ -72,7 +72,7 @@ function buildBackupCounts(backups: readonly PBSBackup[]): Map {
function serverIsOnline(resource: Resource): boolean {
const status = (resource.status ?? '').toLowerCase();
- const conn = (resource.pbs?.connectionHealth ?? '').toLowerCase();
+ const conn = (resource.pbs?.connectionHealth ?? '').trim().toLowerCase();
if (conn) return conn === 'healthy' || conn === 'ok';
return status === 'online' || status === 'running';
}
@@ -83,10 +83,39 @@ function connectionLabel(resource: Resource): string {
return serverIsOnline(resource) ? 'Online' : 'Offline';
}
-function usagePercent(datastore: ResourcePBSDatastore): number | undefined {
- if (typeof datastore.usagePercent === 'number') return datastore.usagePercent;
- if (datastore.total > 0) return (datastore.used / datastore.total) * 100;
- return undefined;
+// PBS retains a named datastore when its status read fails, with zero-valued
+// capacity and explicit unavailable/error evidence. Those zeros are not an
+// empty datastore. Reachability alone cannot establish a successful store read.
+function datastoreCapacity(datastore: ResourcePBSDatastore | undefined, online: boolean) {
+ if (!datastore) return { state: 'absent' as const, percent: undefined };
+ const status =
+ typeof datastore.status === 'string' ? datastore.status.trim().toLowerCase() : datastore.status;
+ if (
+ !online ||
+ (typeof datastore.error === 'string' && Boolean(datastore.error.trim())) ||
+ ['unavailable', 'offline', 'error', 'failed'].includes(status ?? '')
+ ) {
+ return { state: 'unavailable' as const, percent: undefined };
+ }
+ // Empty status is supported by older payloads; an unrecognised explicit
+ // status must not be interpreted as current healthy capacity.
+ if (
+ (status && !['available', 'online', 'healthy', 'ok'].includes(status)) ||
+ !Number.isFinite(datastore.total) ||
+ datastore.total <= 0 ||
+ !Number.isFinite(datastore.used) ||
+ datastore.used < 0
+ ) {
+ return { state: 'unknown' as const, percent: undefined };
+ }
+ const percent =
+ datastore.usagePercent !== undefined
+ ? datastore.usagePercent
+ : (datastore.used / datastore.total) * 100;
+ if (!Number.isFinite(percent) || percent < 0) {
+ return { state: 'unknown' as const, percent: undefined };
+ }
+ return { state: 'available' as const, percent };
}
// >=90% is the silent-backup-failure danger zone; >=75% is the early warning.
@@ -480,7 +509,12 @@ export function ProxmoxBackupServersTable(props: {
estimatedRowHeight={32}
>
{(row) => {
- const pct = () => (row.datastore ? usagePercent(row.datastore) : undefined);
+ const capacity = () => datastoreCapacity(row.datastore, row.online);
+ const pct = () => capacity().percent;
+ const capacityNotice = () =>
+ capacity().state === 'unavailable'
+ ? 'Datastore capacity is unavailable. This does not mean the datastore is empty.'
+ : 'Datastore capacity is unknown. This does not mean the datastore is empty.';
const rowIdentity = { id: row.key };
const isExpanded = () => detail.isExpanded(rowIdentity);
const detailRowId = () => detail.detailRowId(rowIdentity);
@@ -510,13 +544,18 @@ export function ProxmoxBackupServersTable(props: {
-
+
+
+
-
+ >
{row.connectionLabel}
@@ -579,6 +618,7 @@ export function ProxmoxBackupServersTable(props: {
{row.datastore?.name ?? '—'}
@@ -589,25 +629,39 @@ export function ProxmoxBackupServersTable(props: {
fallback={No datastore data}
>
{(datastore) => (
-
-
-
-
-
-
-
-
-
- {formatPlatformTableBytesValue(datastore().used, '0 B')} /{' '}
- {formatPlatformTableBytesValue(datastore().total)}
+
+ {capacity().state === 'unavailable' ? 'Unavailable' : 'Unknown'}
-
-
+ }
+ >
+
+
+
+
+
+
+
+
+
+ {formatPlatformTableBytesValue(datastore().used, '0 B')} /{' '}
+ {formatPlatformTableBytesValue(datastore().total)}
+
+
+
+
)}
@@ -645,10 +699,15 @@ export function ProxmoxBackupServersTable(props: {
class={`${getPlatformTableCellClassForKind('numeric-value')} text-muted tabular-nums text-[11px]`}
>
0
+ }
fallback={—}
>
- {(factor) => <>{factor().toFixed(1)}×>}
+ {row.datastore?.deduplicationFactor?.toFixed(1)}×
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
new file mode 100644
index 0000000000..a3861347a7
--- /dev/null
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
@@ -0,0 +1,146 @@
+import { cleanup, render, waitFor, within } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { Resource, ResourcePBSDatastore } from '@/types/resource';
+import { canonicalizeRealtimeResource } from '@/utils/resourceStateAdapters';
+import { ProxmoxBackupServersTable } from '../ProxmoxBackupServersTable';
+
+vi.mock('@/components/Infrastructure/ResourceDetailDrawer', () => ({
+ ResourceDetailDrawer: () => Details
,
+}));
+
+afterEach(cleanup);
+
+const datastore = (patch: Record = {}): ResourcePBSDatastore =>
+ ({
+ name: 'tank',
+ total: 1000,
+ used: 400,
+ available: 600,
+ status: 'available',
+ deduplicationFactor: 2,
+ ...patch,
+ }) as ResourcePBSDatastore;
+
+const server = (store?: ResourcePBSDatastore, health = 'healthy'): Resource => ({
+ id: 'pbs-capacity',
+ type: 'pbs',
+ name: 'pbs-capacity',
+ status: 'online',
+ platformId: 'pbs-capacity',
+ platformType: 'proxmox-pbs',
+ sourceType: 'api',
+ lastSeen: 1_790_000_000_000,
+ pbs: { instanceId: 'pbs-capacity', connectionHealth: health, datastores: store ? [store] : [] },
+});
+
+function usedCell(container: HTMLElement) {
+ const table = container.querySelector('table')!;
+ const index = Array.from(table.querySelectorAll('th')).findIndex(
+ (head) => head.textContent === 'Used',
+ );
+ return table.querySelector('tbody tr')!.querySelectorAll('td')[index] as HTMLElement;
+}
+
+describe('PBS datastore capacity evidence', () => {
+ it.each([
+ [
+ 'provider failure zeros',
+ { total: 0, used: 0, available: 0, usagePercent: 0, status: 'unavailable' },
+ 'Unavailable',
+ ],
+ ['retained failure values', { status: 'unavailable', usagePercent: 40 }, 'Unavailable'],
+ [
+ 'explicit error with healthy status',
+ { error: 'PRIVATE_PROVIDER_ERROR_SENTINEL' },
+ 'Unavailable',
+ ],
+ ['unknown status', { status: 'unknown', usagePercent: 0 }, 'Unknown'],
+ ['future status', { status: 'new-provider-state' }, 'Unknown'],
+ ['zero total', { total: 0, used: 0, usagePercent: 0 }, 'Unknown'],
+ ['missing used', { used: undefined }, 'Unknown'],
+ ['negative used', { used: -10 }, 'Unknown'],
+ ['nonfinite total', { total: Infinity }, 'Unknown'],
+ ['invalid authoritative percent', { usagePercent: NaN }, 'Unknown'],
+ ['negative authoritative percent', { usagePercent: -1 }, 'Unknown'],
+ ['null authoritative percent', { usagePercent: null }, 'Unknown'],
+ ['string authoritative percent', { usagePercent: '0' }, 'Unknown'],
+ ['overflowed ratio', { used: Number.MAX_VALUE, total: Number.MIN_VALUE }, 'Unknown'],
+ ])('shows %s as unknown evidence rather than healthy usage', async (_name, patch, label) => {
+ const { container } = render(() => (
+ 1200} />
+ ));
+ const cell = usedCell(container);
+ await waitFor(() => expect(within(cell).getByText(label)).toBeInTheDocument());
+ expect(cell.textContent).not.toMatch(/%|0 B/);
+ expect(cell.querySelector('.bg-emerald-500')).toBeNull();
+ expect(cell.querySelector('[title]')?.getAttribute('title')).toContain(
+ 'does not mean the datastore is empty',
+ );
+ expect(container.textContent).not.toContain('2.0×');
+ expect(container.innerHTML).not.toContain('PRIVATE_PROVIDER_ERROR_SENTINEL');
+ expect(within(container).getByText('Healthy')).toBeInTheDocument();
+ });
+
+ it('withdraws retained capacity and dedup when the PBS connection is offline', () => {
+ const { container } = render(() => (
+ 1200}
+ />
+ ));
+ expect(usedCell(container).textContent).toBe('Unavailable');
+ expect(container.textContent).not.toContain('2.0×');
+ });
+
+ it.each([
+ [0, '0.0%', 'bg-emerald-500'],
+ [750, '75.0%', 'bg-amber-500'],
+ [900, '90.0%', 'bg-red-500'],
+ [1100, '110.0%', 'bg-red-500'],
+ ])('keeps measured usage %s and the existing warning thresholds', async (used, label, tone) => {
+ const { container } = render(() => (
+ 1200} />
+ ));
+ const cell = usedCell(container);
+ await waitFor(() => expect(within(cell).getByText(label)).toBeInTheDocument());
+ expect(cell.querySelector(`.${tone}`)).not.toBeNull();
+ expect(container.textContent).toContain('2.0×');
+ });
+
+ it('keeps a server without datastore inventory distinct from an unknown named store', () => {
+ const { container } = render(() => (
+ 1200} />
+ ));
+ expect(usedCell(container).textContent).toBe('No datastore data');
+ });
+
+ it.each(['direct', 'canonical'] as const)(
+ 'reconciles same-identity %s failure and recovery without remounting',
+ async (transport) => {
+ const present = (store: ResourcePBSDatastore): Resource => {
+ const resource = server(store);
+ if (transport === 'direct') return resource;
+ const { pbs, ...base } = resource;
+ return canonicalizeRealtimeResource({ ...base, platformData: { sources: ['pbs'], pbs } });
+ };
+ const [servers, setServers] = createSignal([present(datastore())]);
+ const { container } = render(() => (
+ 1200} />
+ ));
+ const cell = usedCell(container);
+ await waitFor(() => expect(cell.textContent).toContain('40.0%'));
+ setServers([
+ present(datastore({ total: 0, used: 0, usagePercent: 0, status: 'unavailable' })),
+ ]);
+ await waitFor(() => expect(cell.textContent).toBe('Unavailable'));
+ expect(usedCell(container)).toBe(cell);
+ setServers([present(datastore({ used: 0, usagePercent: 0 }))]);
+ await waitFor(() => expect(cell.textContent).toContain('0.0%'));
+ expect(usedCell(container)).toBe(cell);
+ setServers([present(datastore({ used: 950, usagePercent: 95 }))]);
+ await waitFor(() => expect(cell.textContent).toContain('95.0%'));
+ expect(cell.querySelector('.bg-red-500')).not.toBeNull();
+ },
+ );
+});
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.drawer.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.drawer.test.tsx
index 05854d4787..4dd8562b69 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.drawer.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.drawer.test.tsx
@@ -100,7 +100,9 @@ describe('ProxmoxBackupServersTable details', () => {
const [servers, setServers] = createSignal(makeServers());
const { container } = render(() => );
const datastoreNames = () =>
- Array.from(container.querySelectorAll('td[title]')).map((cell) => cell.getAttribute('title'));
+ Array.from(container.querySelectorAll('tbody tr td:first-child[title]')).map((cell) =>
+ cell.getAttribute('title'),
+ );
fireEvent.click(screen.getAllByRole('button', { name: 'Expand details for pbs-main' })[1]);
const detail = screen.getByTestId('pbs-resource-detail');
expect(datastoreNames()).toEqual(['pbs-main · archive', 'pbs-main · tank']);
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts
index 6dfa3b16c3..222caf0d18 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts
@@ -30,6 +30,25 @@ const makePbsResource = (overrides: Partial = {}): Resource =>
}) as Resource;
describe('buildBackupServerRows', () => {
+ it('retains failed datastore-read evidence without changing server or row identity', () => {
+ const server = makePbsResource();
+ server.pbs!.datastores![0] = {
+ name: 'tank',
+ total: 0,
+ used: 0,
+ available: 0,
+ usagePercent: 0,
+ status: 'unavailable',
+ error: 'synthetic status failure',
+ };
+ const rows = buildBackupServerRows([server]);
+ const tank = rows.find((row) => row.datastore?.name === 'tank')!;
+ expect(tank.key).toBe('pbs-1:tank');
+ expect(tank.online).toBe(true);
+ expect(tank.datastore).toEqual(server.pbs!.datastores![0]);
+ expect(rows.find((row) => row.datastore?.name === 'offsite')?.datastore?.usagePercent).toBe(95);
+ });
+
it('carries host CPU, memory, and uptime onto every datastore row of the server', () => {
const rows = buildBackupServerRows([makePbsResource()]);
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
index 2b6d786cc0..d047855666 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
@@ -214,6 +214,52 @@ beforeEach(() => {
});
describe('ProxmoxBackupsTable', () => {
+ it('keeps successful backup inventory independent of unavailable datastore capacity', async () => {
+ mockBackupAPIs();
+ const failed = {
+ ...pbsServerResource,
+ pbs: {
+ ...pbsServerResource.pbs!,
+ datastores: [
+ {
+ name: 'main',
+ total: 0,
+ used: 0,
+ available: 0,
+ usagePercent: 0,
+ status: 'unavailable',
+ error: 'PRIVATE_PROVIDER_ERROR_SENTINEL',
+ },
+ ],
+ },
+ } as Resource;
+ const [servers, setServers] = createSignal([failed]);
+ const { container } = renderInRouter(() => (
+ }
+ workloads={[workloadResource]}
+ servers={servers()}
+ />
+ ));
+ const table = container.querySelector('[data-proxmox-backups-table="servers"]') as HTMLElement;
+ const backupCount = () => {
+ const index = Array.from(table.querySelectorAll('th')).findIndex(
+ (head) => head.getAttribute('aria-label') === 'Backups',
+ );
+ return table.querySelector('tbody tr')!.querySelectorAll('td')[index].textContent;
+ };
+ await within(table).findByText('Unavailable');
+ await waitFor(() => expect(backupCount()).toBe('1'));
+ await screen.findAllByText('Protected');
+ expect(container.innerHTML).not.toContain('PRIVATE_PROVIDER_ERROR_SENTINEL');
+ expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/backups/pbs')).toHaveLength(1);
+
+ setServers([pbsServerResource]);
+ await waitFor(() => expect(within(table).queryByText('Unavailable')).not.toBeInTheDocument());
+ expect(backupCount()).toBe('1');
+ expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/backups/pbs')).toHaveLength(1);
+ });
+
it('uses a corroborated PBS host link for Backups History despite a PVE-only name collision', () => {
const pbs = {
...pbsServerResource,
diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts
index ac7c723426..065410f49a 100644
--- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts
+++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts
@@ -46,8 +46,9 @@ describe('Proxmox backups responsive table presentation', () => {
'used',
]);
expect(getBackupServerColumnWidthStyle('server', 'compact')).toEqual({ width: '40%' });
- expect(getBackupServerColumnWidthStyle('status', 'compact')).toEqual({ width: '23%' });
- expect(getBackupServerColumnWidthStyle('used', 'compact')).toEqual({ width: '15%' });
+ expect(getBackupServerColumnWidthStyle('status', 'compact')).toEqual({ width: '18%' });
+ expect(getBackupServerColumnWidthStyle('used', 'compact')).toEqual({ width: '25%' });
+ expect(getBackupServerColumnWidthStyle('datastore', 'compact')).toEqual({ width: '17%' });
expect(ids(getBackupServerColumns('basic'))).toEqual([
'server',
'status',
diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts b/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts
index e2299253bc..f62754f695 100644
--- a/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts
+++ b/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts
@@ -107,7 +107,9 @@ const BACKUP_SERVER_WEIGHTS: Record<
ProxmoxBackupsTableLayoutMode,
Partial>
> = {
- compact: { server: 40, status: 23, datastore: 22, used: 15 },
+ // Capacity must fit "Unavailable", not just a short percentage. Keep the
+ // shared 40% identity anchor; the compact status omits its redundant dot.
+ compact: { server: 40, status: 18, datastore: 17, used: 25 },
// Just above the phone projection the used cell carries its used/total
// pair again, the longest value in the row, so it takes the most room.
basic: { server: 24, status: 13.6, datastore: 18.2, used: 28.4, backups: 9.8 },
diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts
index c79d1900d0..36899574e6 100644
--- a/frontend-modern/src/types/resource.ts
+++ b/frontend-modern/src/types/resource.ts
@@ -466,6 +466,7 @@ export interface ResourcePBSDatastore {
available?: number;
usagePercent?: number;
status?: string;
+ error?: string;
deduplicationFactor?: number;
}
From 3970960d318db7ba5a5a5230b3c42051540f1e28 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 11:58:40 +0100
Subject: [PATCH 2/7] Wait for routed backup capacity evidence in the page
control
Bind the independent-inventory assertions only after the routed server table mounts. Preserve failure, protection, count and recovery assertions; no production or contract change.
Change-source: pulse-maintainer
---
.../features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
index d047855666..dc321ce203 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
@@ -241,6 +241,9 @@ describe('ProxmoxBackupsTable', () => {
servers={servers()}
/>
));
+ // The router mounts its page asynchronously. Wait for the capacity notice
+ // before binding page-level queries to the server table.
+ await screen.findByText('Unavailable');
const table = container.querySelector('[data-proxmox-backups-table="servers"]') as HTMLElement;
const backupCount = () => {
const index = Array.from(table.querySelectorAll('th')).findIndex(
From 691ced6926f3a320bbf18afd740becfd47b50e79 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:05:19 +0100
Subject: [PATCH 3/7] Guard phone theme setup before the document root exists
Keep all browser error and geometry assertions; theme setup must tolerate the early WebKit init-script phase. The existing post-load dark-context assertion setup remains unchanged.
Change-source: pulse-maintainer
---
frontend-modern/browser-tests/pbs-capacity.cjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend-modern/browser-tests/pbs-capacity.cjs b/frontend-modern/browser-tests/pbs-capacity.cjs
index 5ba31895d6..68eba42afd 100644
--- a/frontend-modern/browser-tests/pbs-capacity.cjs
+++ b/frontend-modern/browser-tests/pbs-capacity.cjs
@@ -52,7 +52,7 @@ const runtime = [
hasTouch: phone,
colorScheme: phone ? 'dark' : 'light',
});
- if (phone) await page.addInitScript(() => document.documentElement.classList.add('dark'));
+ if (phone) await page.addInitScript(() => document.documentElement?.classList.add('dark'));
page.setDefaultTimeout(20000);
await page.clock.setFixedTime(new Date('2026-10-04T12:00:00Z'));
const errors = [],
From ccb296676c167d2b948d96a54319bc0d7b52549a Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:11:26 +0100
Subject: [PATCH 4/7] Bind PBS render verification to table and font readiness
Keep the 20-second deadline and all page-error, privacy, state, identity and geometry assertions. Await DOM readiness, the actual table and loaded fonts rather than treating the dev server's generic load event as the product acceptance condition. Retain the earlier load-event timeout.
Change-source: pulse-maintainer
---
frontend-modern/browser-tests/pbs-capacity.cjs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/frontend-modern/browser-tests/pbs-capacity.cjs b/frontend-modern/browser-tests/pbs-capacity.cjs
index 68eba42afd..82c3816180 100644
--- a/frontend-modern/browser-tests/pbs-capacity.cjs
+++ b/frontend-modern/browser-tests/pbs-capacity.cjs
@@ -95,12 +95,17 @@ const runtime = [
body: JSON.stringify({ data }),
});
});
- await page.goto(`${origin}/browser-tests/pbs-capacity.html?transport=${transport}`);
+ // Verify the actual table and its font metrics, not the dev server's
+ // generic load event. Keep the same bounded navigation/locator deadline.
+ await page.goto(`${origin}/browser-tests/pbs-capacity.html?transport=${transport}`, {
+ waitUntil: 'domcontentloaded',
+ });
// Imported production theme stores can initialise after addInitScript.
// Apply the requested CSS context after the fixture is loaded too.
if (phone) await page.evaluate(() => document.documentElement.classList.add('dark'));
const table = page.locator('[data-proxmox-backups-table="servers"]');
await table.waitFor();
+ await page.waitForFunction(() => document.fonts.status === 'loaded');
const head = await table.locator('th').allTextContents();
const usedIndex = head.indexOf('Used');
assert(usedIndex >= 0);
From 99256377e1bac7f1668db2cf3ed81f343ba26ee7 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:21:48 +0100
Subject: [PATCH 5/7] Complete typed PBS capacity controls and rendered proof
Supply the existing required resource display names without changing rendered labels. Bind the six completed desktop and touch context controls to the exact production content, retaining the earlier adverse init and load observations.
Change-source: pulse-maintainer
---
.../browser-tests/pbs-capacity.tsx | 1 +
.../55462e72c185a9e6.json | 54 +++++++++++++++++++
...roxmoxBackupServersTable.capacity.test.tsx | 1 +
3 files changed, 56 insertions(+)
create mode 100644 frontend-modern/browser-verification/55462e72c185a9e6.json
diff --git a/frontend-modern/browser-tests/pbs-capacity.tsx b/frontend-modern/browser-tests/pbs-capacity.tsx
index 7dab6d9930..df4a4018e2 100644
--- a/frontend-modern/browser-tests/pbs-capacity.tsx
+++ b/frontend-modern/browser-tests/pbs-capacity.tsx
@@ -40,6 +40,7 @@ function Fixture() {
id: 'pbs-main',
type: 'pbs',
name: 'pbs-main',
+ displayName: 'pbs-main',
platformId: 'pbs-main',
platformType: 'proxmox-pbs',
sourceType: 'api',
diff --git a/frontend-modern/browser-verification/55462e72c185a9e6.json b/frontend-modern/browser-verification/55462e72c185a9e6.json
new file mode 100644
index 0000000000..5fb3f948a9
--- /dev/null
+++ b/frontend-modern/browser-verification/55462e72c185a9e6.json
@@ -0,0 +1,54 @@
+{
+ "version": 1,
+ "base_sha": "ccb296676c167d2b948d96a54319bc0d7b52549a",
+ "verified_at": "2026-10-04T11:21:10.896992Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
+ "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts",
+ "frontend-modern/src/types/resource.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "5d51f3fe36638bde58f0d31b550947a78adc664dc0898ca323aac4fb8db62903",
+ "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts": "00bdd77055a4c1c76bd532361f5bf0905aa253a6a72adf630445eb319f4843d0",
+ "frontend-modern/src/types/resource.ts": "007885931ae929a05ce382fa70d9afd3ed784d5ca32fc4ccf403260675a5a6de"
+ },
+ "routes": [
+ "Local /browser-tests/pbs-capacity.html: production ProxmoxBackupsTable/router/CSS, direct and canonical synthetic resources"
+ ],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900,
+ "browser": "Chromium",
+ "input": "keyboard",
+ "colour": "light"
+ },
+ {
+ "width": 390,
+ "height": 844,
+ "browser": "WebKit",
+ "input": "touch",
+ "colour": "dark"
+ },
+ {
+ "width": 320,
+ "height": 844,
+ "browser": "WebKit",
+ "input": "touch",
+ "colour": "dark"
+ }
+ ],
+ "states": [
+ "Healthy capacity \u2192 failed zero capacity \u2192 unknown \u2192 offline retained values \u2192 safe fixed error notice",
+ "Measured zero remains zero; 95% remains danger; missing datastore stays separate; ordinary readings resume",
+ "All six content-identical product contexts complete; earlier overall init/load failures remain adverse"
+ ],
+ "interactions": [
+ "Desktop keyboard Enter and phone taps switch synthetic observations in the existing page",
+ "Same-identity notice updates preserve the mounted capacity cell; no false success dot, raw error or unreliable deduplication",
+ "Font-aware warning bounds and page overflow checked; all six screenshots inspected",
+ "No external-origin request or API mutation"
+ ],
+ "evidence": "Six exact-content context controls across two retained attempts, not a claim that every browser attempt passed. See browser-context-matrix.json in the lane dossier. Earlier generic-load timeout is not cleared or re-described as a timing pass."
+}
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
index a3861347a7..3acc64cbc1 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.capacity.test.tsx
@@ -26,6 +26,7 @@ const server = (store?: ResourcePBSDatastore, health = 'healthy'): Resource => (
id: 'pbs-capacity',
type: 'pbs',
name: 'pbs-capacity',
+ displayName: 'pbs-capacity',
status: 'online',
platformId: 'pbs-capacity',
platformType: 'proxmox-pbs',
From 795f0b4863e46b81584c82152cc7e66974e5c9a1 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:31:46 +0100
Subject: [PATCH 6/7] Distinguish datastore read evidence from shared capacity
adapters
Name the component-specific fault-admission accessor explicitly instead of shadowing the generic adapter capacity helper. The shared snapshot constructor does not decide this read-evidence policy. Keep the audit and all display semantics unchanged.
Change-source: pulse-maintainer
Contract-Neutral: Names a component-specific datastore read-evidence accessor distinctly from the shared capacity-snapshot adapter; no behaviour, input, wire or public-contract delta.
---
.../proxmox/ProxmoxBackupServersTable.tsx | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
index 8cb2aa0be7..18b98637e8 100644
--- a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
+++ b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx
@@ -509,10 +509,11 @@ export function ProxmoxBackupServersTable(props: {
estimatedRowHeight={32}
>
{(row) => {
- const capacity = () => datastoreCapacity(row.datastore, row.online);
- const pct = () => capacity().percent;
+ // Read admission is not a generic storage-adapter capacity snapshot.
+ const capacityEvidence = () => datastoreCapacity(row.datastore, row.online);
+ const pct = () => capacityEvidence().percent;
const capacityNotice = () =>
- capacity().state === 'unavailable'
+ capacityEvidence().state === 'unavailable'
? 'Datastore capacity is unavailable. This does not mean the datastore is empty.'
: 'Datastore capacity is unknown. This does not mean the datastore is empty.';
const rowIdentity = { id: row.key };
@@ -630,13 +631,15 @@ export function ProxmoxBackupServersTable(props: {
>
{(datastore) => (
- {capacity().state === 'unavailable' ? 'Unavailable' : 'Unknown'}
+ {capacityEvidence().state === 'unavailable'
+ ? 'Unavailable'
+ : 'Unknown'}
}
>
@@ -700,7 +703,7 @@ export function ProxmoxBackupServersTable(props: {
>
0
From d2f7e456b3a55719e58c794dd1069d67ce3c826b Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:37:52 +0100
Subject: [PATCH 7/7] Verify final PBS capacity evidence on desktop and phones
Bind the final production content to six completed Chromium/WebKit direct and canonical controls, with inspected 1365px, 390px and 320px screens, mounted replacements and safe error notices. Retain earlier failed executions separately.
Change-source: pulse-maintainer
---
.../5f82921afb9be39d.json | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 frontend-modern/browser-verification/5f82921afb9be39d.json
diff --git a/frontend-modern/browser-verification/5f82921afb9be39d.json b/frontend-modern/browser-verification/5f82921afb9be39d.json
new file mode 100644
index 0000000000..e2a13e6366
--- /dev/null
+++ b/frontend-modern/browser-verification/5f82921afb9be39d.json
@@ -0,0 +1,54 @@
+{
+ "version": 1,
+ "base_sha": "795f0b4863e46b81584c82152cc7e66974e5c9a1",
+ "verified_at": "2026-10-04T11:36:56.829475Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
+ "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts",
+ "frontend-modern/src/types/resource.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "d2063f2d535c08980f57708a9cd4871f5cd1b98de1bb007c3eb78940463a2ded",
+ "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts": "00bdd77055a4c1c76bd532361f5bf0905aa253a6a72adf630445eb319f4843d0",
+ "frontend-modern/src/types/resource.ts": "007885931ae929a05ce382fa70d9afd3ed784d5ca32fc4ccf403260675a5a6de"
+ },
+ "routes": [
+ "Local /browser-tests/pbs-capacity.html: production ProxmoxBackupsTable/router/CSS, direct and canonical synthetic resources"
+ ],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900,
+ "browser": "Chromium",
+ "input": "keyboard",
+ "colour": "light"
+ },
+ {
+ "width": 390,
+ "height": 844,
+ "browser": "WebKit",
+ "input": "touch",
+ "colour": "dark"
+ },
+ {
+ "width": 320,
+ "height": 844,
+ "browser": "WebKit",
+ "input": "touch",
+ "colour": "dark"
+ }
+ ],
+ "states": [
+ "Healthy capacity \u2192 failed zero capacity \u2192 unknown \u2192 offline retained values \u2192 safe fixed error notice",
+ "Measured zero remains zero; 95% remains danger; missing datastore stays separate; ordinary readings resume",
+ "All six desktop/touch product contexts complete in one final execution; earlier init/load/source failures retained separately"
+ ],
+ "interactions": [
+ "Desktop keyboard Enter and phone taps switch synthetic observations in the existing page",
+ "Same-identity notice updates preserve the mounted capacity cell; no false success dot, raw error or unreliable deduplication",
+ "Font-aware warning bounds and page overflow checked; all six screenshots inspected",
+ "No external-origin request or API mutation"
+ ],
+ "evidence": "Final complete six-context execution, source-bound by browser-qualified-binding/result in the lane dossier. Earlier generic-load timeout and all source failures are retained, not cleared or claimed as timing passes."
+}