From 852ebdca080535c10b2d10dd39465953cd47b8ef Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:20:55 +0100 Subject: [PATCH] Keep Proxmox backup and node tables whole on phones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a 390px phone the Proxmox tables clipped the values users came for. The nodes table showed six "Anal…" rows for the Analytics A–F cluster: a sixth column (temperature) left the node name four characters wide with the toggle and status dot in front of it. The backup server row showed "H…" for Healthy, and the coverage and By date rows clipped "18h ago" to "18 …" and "Running" to "Runni…". Nodes now carry the same five columns as the guest table beneath them with a wider identity cell; temperature returns from 440px, where it fits. Backup ages use a suffix-free compact form ("18h") with the full timestamp on hover. The server row keeps its status word and used percentage whole and lets the backup count return at the next width, and its stale header width classes no longer fight the colgroup. The By date row keeps its source and state badges whole while the repository moves to the row's hover title. Just above the phone projection the servers' used/total pair and the coverage posture word now fit, and at desktop width "Automation Lab Standalone" no longer truncates in the nodes table. Measured with a scrollWidth-over-clientWidth sweep of every table cell at 1440px, 560px and 390px; only identity strings still clip, each with its full text on hover. Contract-Neutral: phone and mid-width column projections and labels only; owning unified-resources clause updated; no storage-recovery authority or frontend-primitives change --- .../internal/subsystems/unified-resources.md | 12 ++++++ frontend-modern/browser-verification.json | 41 +++++++++++-------- .../proxmox/ProxmoxBackupServersTable.tsx | 32 ++++++--------- .../features/proxmox/ProxmoxCoverageTable.tsx | 22 ++++++++-- .../proxmox/ProxmoxRecoverableTable.tsx | 5 ++- .../ProxmoxRecoverableTable.test.tsx | 5 ++- .../proxmoxBackupsTablePresentation.test.ts | 7 ++-- .../proxmoxBackupsTableShared.test.tsx | 24 +++++++++++ .../__tests__/proxmoxHostTableModel.test.ts | 12 +++--- .../proxmoxBackupsTablePresentation.ts | 26 ++++++++---- .../proxmox/proxmoxBackupsTableShared.tsx | 24 +++++++++-- .../features/proxmox/proxmoxHostTableModel.ts | 33 +++++++++------ 12 files changed, 166 insertions(+), 77 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index ae311147f4..7ae2bf6b82 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -842,6 +842,18 @@ the daemon tally as width shrinks, keeps one five-value phone projection (cluster, health, OSDs, pools, capacity) whose values each fit whole, and leaves quorum membership and the FSID to the cluster disclosure, where the FSID stays searchable and is shown in full. +The Proxmox host table drops temperature from its phone projection +(container widths under 440px) so the node name, expansion toggle, and +status dot share the same five-column shape as the guest table beneath it, +and the identity cell takes a wider phone anchor than the shared default +because of that leading toggle. `ProxmoxBackupServersTable`, +`ProxmoxCoverageTable`, and `ProxmoxRecoverableTable` keep their phone +projections to values that fit whole: the server row keeps its status word +and used percentage and defers the backup count to the next width, backup +ages use a suffix-free compact form with the timestamp on hover, and the +recoverable row keeps its source and state badges while the repository +location moves to the row title. Header width classes must not compete with +the colgroup those tables size through the shared weighted helper. Optional numeric table cells follow the same split: unified-resource consumers own which count or replica field is meaningful, whether the domain should zero-default an absent scheduler/service/inventory count, whether a diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 0054969785..6b556a231e 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,23 +1,27 @@ { "version": 1, - "base_sha": "b052d29b19909e8ed2581043426d76bb9d20c2a0", - "verified_at": "2026-10-02T22:15:42Z", + "base_sha": "3f2715e3f3f9b563ad3f119ba0d302b488b97e8a", + "verified_at": "2026-10-02T22:20:55Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", - "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts", - "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts" + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx", + "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts", + "frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx", + "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts" ], "content_sha256": { - "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx": "786be66c40d33006edc1eda234df083ee68e0296103882ff108711026dd2ea29", - "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "075994cbe8d47c47a95747ab4d5181c11666cf6272ec8fbe6026b5567de10dfb", - "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts": "f001412aff663f53ed5ae4a64ebcaa381257dbb3c19497221cbebb77ee5fab08", - "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts": "dc2f9dcfbd51eb311596ff12a2588a645394e9ab717595a2886b41c89fae5c95" + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "adbae773cc485f5dc16d1d316bbd5e1b1ace6be24404861ce02bf7dd8890bb25", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "ce0bf7e73d500c09a61df52b7c948428d83cebf6852ae737f7bcdb7ee0997ae0", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx": "7b7aea630dda12202b5734cf94d4c3409d6dc21b632efe918e7391d4b150e80c", + "frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts": "0037e6f97e1180ef0c0d585ec5e9385dcbe4c4e1b51a7bc29d8631b4b0e87745", + "frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx": "2ab0f6c58e18337f05ea85082ed5c465a625c5388ff82987eec41bbbe3005915", + "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "4aab84754245dda70c3b3be3e81b15875a8664d38c8d0325329b2074b6c26fd5" }, "routes": [ + "/proxmox/overview", "/proxmox/backups/coverage", - "/proxmox/backups/coverage?posture=unprotected", "/proxmox/backups/date" ], "viewports": [ @@ -35,15 +39,16 @@ } ], "states": [ - "Mock estate: coverage rows whose newest guest snapshot is fresher than their PBS/PVE backup now show the backup age under Last backup instead of the snapshot age", - "Snapshot-only rows show None under Last backup with the posture engine's snapshot-only reason as the hover title", - "Rows with no artifacts show None with the no-backup title; attention rows with failed tasks keep their backup age", - "Phone layout keeps the Age header and the same values; the By date view and Backup Health strip are unchanged" + "Nodes table at 390px shows five columns (node, CPU, memory, disk, age) with Analytics A-F names whole; temperature returns at 560px and 1440px", + "Backup server rows at 390px show server, Healthy, datastore and used percentage whole; the backup count returns at 560px with its used/total pair whole", + "Coverage rows at 390px show whole compact ages (2h, 1d), the whole compact posture word, and the job dot; at 560px the posture word and Backup header fit", + "By date rows at 390px keep source and state badges whole with the repository in the row title; at 1440px Automation Lab Standalone no longer truncates in the nodes table", + "scrollWidth-over-clientWidth sweep of every table cell at the three widths: only identity strings (datastore names, long node names) clip, each with its full text on hover" ], "interactions": [ - "Sorted by Last backup at 1440px and by Age at 390px and read the first row", - "Read the None cell hover titles for unprotected rows at both widths", - "Expanded a coverage row to its restore evidence at both widths", - "Read coverage row cells through the DOM at 1440px and compared them with the Posture explanation" + "Expanded the first node row and the first backup server row at each width", + "Sorted the coverage table by its age column at each width and read the first row", + "Read the By date first-row hover title at each width", + "Read the node table column widths and the node cell layout through the DOM at 390px, 360px and 430px" ] } diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx index 313270eca0..79c59775f0 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx @@ -422,14 +422,10 @@ export function ProxmoxBackupServersTable(props: { } header={ <> - + - + @@ -449,20 +445,16 @@ export function ProxmoxBackupServersTable(props: { - + - + Used @@ -597,12 +589,14 @@ export function ProxmoxBackupServersTable(props: { > {(datastore) => (
- + + + diff --git a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx index b614968f74..87bc2eb4ce 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx @@ -244,7 +244,13 @@ export function ProxmoxCoverageTable(props: { headClass={getPlatformTableHeadClassForKind('text')} /> } > - {(artifact) => } + {(artifact) => ( + + )} @@ -440,7 +451,12 @@ export function ProxmoxCoverageTable(props: { } > - {(artifact) => } + {(artifact) => ( + + )} diff --git a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx index 6da1dd7948..6457d00b66 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx @@ -123,6 +123,9 @@ export function ProxmoxRecoverableTable(props: { artifact.workload.name || artifact.workload.label, artifact.workload.typeLabel, artifact.workload.vmid, + // The phone projection has no location column, so the row's + // hover title carries the repository instead. + columnVisible('location') ? undefined : artifact.location, ] .filter(Boolean) .join(' · ')} @@ -163,7 +166,7 @@ export function ProxmoxRecoverableTable(props: { - + diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxRecoverableTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxRecoverableTable.test.tsx index 0dea167612..a8c0248ff5 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxRecoverableTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxRecoverableTable.test.tsx @@ -55,12 +55,13 @@ describe('ProxmoxRecoverableTable responsive columns', () => { expect([...document.querySelectorAll('thead th')].map((th) => th.textContent?.trim())).toEqual([ 'Workload', 'Via', - 'Loc', 'Age▼', 'State', ]); expect(document.body.textContent).toContain('VM 100'); - expect(document.body.textContent).toContain('main / pve1'); + // The phone projection has no location column; the row title carries it. + expect(document.body.textContent).not.toContain('main / pve1'); + expect(document.querySelector('td [title*="main / pve1"]')).not.toBeNull(); expect(document.body.textContent).not.toContain('5 PBS files'); }); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts index 8817787792..a52bd7c075 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTablePresentation.test.ts @@ -44,10 +44,10 @@ describe('Proxmox backups responsive table presentation', () => { 'status', 'datastore', 'used', - 'backups', ]); expect(getBackupServerColumnWidthStyle('server', 'compact')).toEqual({ width: '40%' }); - expect(getBackupServerColumnWidthStyle('used', 'compact')).toEqual({ width: '20%' }); + expect(getBackupServerColumnWidthStyle('status', 'compact')).toEqual({ width: '23%' }); + expect(getBackupServerColumnWidthStyle('used', 'compact')).toEqual({ width: '15%' }); expect(ids(getBackupServerColumns('basic'))).toEqual([ 'server', 'status', @@ -124,12 +124,11 @@ describe('Proxmox backups responsive table presentation', () => { expect(ids(getRecoverableColumns('compact'))).toEqual([ 'workload', 'source', - 'location', 'created', 'state', ]); expect(getRecoverableColumnWidthStyle('workload', 'compact')).toEqual({ width: '40%' }); - expect(getRecoverableColumnWidthStyle('state', 'compact')).toEqual({ width: '16%' }); + expect(getRecoverableColumnWidthStyle('state', 'compact')).toEqual({ width: '30%' }); expect(ids(getRecoverableColumns('basic'))).toEqual([ 'workload', 'source', diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTableShared.test.tsx b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTableShared.test.tsx index cb34f14569..3694f78460 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTableShared.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupsTableShared.test.tsx @@ -8,8 +8,10 @@ import proxmoxBackupsTableSharedSource from '../proxmoxBackupsTableShared.tsx?ra import { ArtifactSourceBadge, ArtifactStateBadge, + ProxmoxBackupAgeText, ProxmoxBackupWorkloadTypeBadge, SortableHead, + formatCompactBackupAge, } from '../proxmoxBackupsTableShared'; import type { RecoverableArtifact } from '../proxmoxBackupRecoveryModel'; @@ -86,6 +88,28 @@ describe('proxmoxBackupsTableShared', () => { } }); + it('drops the age suffix in the phone projection and keeps the timestamp on hover', () => { + const createdAt = new Date(Date.now() - 18 * 60 * 60 * 1000).toISOString(); + expect(formatCompactBackupAge(createdAt)).toBe('18h'); + expect(formatCompactBackupAge(new Date().toISOString())).toBe('now'); + + const artifact = { + id: 'pbs:1', + nativeId: '1', + sourceKind: 'pbs', + sourceLabel: 'PBS', + workload: { key: 'w', type: 'vm', typeLabel: 'VM', vmid: '100', label: 'VM 100' }, + createdAt, + createdMs: Date.parse(createdAt), + location: 'main', + detail: '', + protected: false, + } as RecoverableArtifact; + render(() => ); + const age = screen.getByText('18h'); + expect(age.closest('[title]')?.getAttribute('title')).toContain(createdAt); + }); + it('keeps backup age text on the shared relative-time primitive', () => { expect(proxmoxBackupsTableSharedSource).toContain('PlatformTableRelativeTimeValue'); expect(proxmoxBackupsTableSharedSource).not.toContain('formatRelativeTime('); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts index 1e23674fb7..305232fb26 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts @@ -19,16 +19,16 @@ describe('proxmoxHostTableModel', () => { expect(getProxmoxHostColumnWidthStyle('uptime', 'narrow', ids)).toEqual({ width: '15%' }); }); - it('shows the full operational metric track at phone width', () => { + it('matches the guest table columns and keeps node names whole at phone width', () => { const columns = getProxmoxHostVisibleColumnsForLayout('phone'); const ids = columns.map((column) => column.id); - expect(ids).toEqual(['node', 'cpu', 'memory', 'disk', 'temp', 'uptime']); + expect(ids).toEqual(['node', 'cpu', 'memory', 'disk', 'uptime']); expect(getProxmoxHostTableMinWidthClass('phone')).toBe('min-w-[0px]'); - expect(getProxmoxHostColumnWidthStyle('node', 'phone', ids)).toEqual({ width: '30%' }); - expect(getProxmoxHostColumnWidthStyle('disk', 'phone', ids)).toEqual({ width: '13.3824%' }); - expect(getProxmoxHostColumnWidthStyle('temp', 'phone', ids)).toEqual({ width: '15.4412%' }); - expect(getProxmoxHostColumnWidthStyle('uptime', 'phone', ids)).toEqual({ width: '14.4118%' }); + expect(getProxmoxHostColumnWidthStyle('node', 'phone', ids)).toEqual({ width: '38%' }); + expect(getProxmoxHostColumnWidthStyle('cpu', 'phone', ids)).toEqual({ width: '15.5%' }); + expect(getProxmoxHostColumnWidthStyle('disk', 'phone', ids)).toEqual({ width: '15.5%' }); + expect(getProxmoxHostColumnWidthStyle('uptime', 'phone', ids)).toEqual({ width: '15.5%' }); }); it('prioritizes live utilization columns in the mobile host table', () => { diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts b/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts index 3f4f02de24..f0ad8ec765 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupsTablePresentation.ts @@ -93,8 +93,10 @@ const BACKUP_SERVER_VISIBLE: Record< > = { // Reachability and datastore exhaustion are the two risks that can stop all // future backups. Host telemetry and space-efficiency context return only as - // the table gains enough room to keep those headline answers readable. - compact: ['server', 'status', 'datastore', 'used', 'backups'], + // the table gains enough room to keep those headline answers readable. The + // phone projection keeps those two answers whole ("Healthy", "33.6%") and + // lets the backup count return with the first wider layout. + compact: ['server', 'status', 'datastore', 'used'], basic: ['server', 'status', 'datastore', 'used', 'backups'], operational: ['server', 'status', 'cpu', 'memory', 'datastore', 'used', 'backups'], expanded: ['server', 'status', 'cpu', 'memory', 'uptime', 'datastore', 'used', 'backups'], @@ -105,8 +107,10 @@ const BACKUP_SERVER_WEIGHTS: Record< ProxmoxBackupsTableLayoutMode, Partial> > = { - compact: { server: 40, status: 15, datastore: 15, used: 20, backups: 10 }, - basic: { server: 24, status: 14, datastore: 20, used: 28, backups: 14 }, + compact: { server: 40, status: 23, datastore: 22, used: 15 }, + // 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 }, operational: { server: 20, status: 12, @@ -198,11 +202,14 @@ const COVERAGE_WEIGHTS: Record< ProxmoxBackupsTableLayoutMode, Partial> > = { - compact: { workload: 40, posture: 18, latest: 16, pbs: 16, task: 10 }, + // Posture keeps its whole compact word ("Unknown"), the two age cells hold + // the suffix-free compact age, and the job dot takes what a "Job" header + // needs. + compact: { workload: 40, posture: 21, latest: 14.5, pbs: 14.5, task: 10 }, // Above the phone projection the identity columns (workload, node) take the // slack that the short type badge, target id, and age cells cannot use, so // names stay whole instead of truncating beside empty space. - basic: { workload: 31, node: 20, posture: 20, latest: 14, task: 15 }, + basic: { workload: 31, node: 18, posture: 22, latest: 15, task: 14 }, operational: { workload: 27, type: 8, node: 17, posture: 17, latest: 15, task: 16 }, expanded: { workload: 19, @@ -287,7 +294,10 @@ const RECOVERABLE_VISIBLE: Record getRecoveryAgeBand(props.artifact.createdMs); const title = () => { const parts = [recoveryAgeTitleByBand[band()], props.artifact.createdAt].filter(Boolean); @@ -228,7 +239,12 @@ export function ProxmoxBackupAgeText(props: { artifact: RecoverableArtifact }) { return ( - + } + > + {formatCompactBackupAge(props.artifact.createdAt)} + ); } diff --git a/frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts b/frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts index b053bdb00f..1a9d7db7cd 100644 --- a/frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts @@ -31,7 +31,7 @@ const HOST_COLUMN_MIN_LAYOUT: Record = { - node: 18, - version: 7, + node: 20, + version: 6, uptime: 7, cpu: 13, memory: 13, @@ -55,7 +55,7 @@ const HOST_COLUMN_DESKTOP_WIDTHS: Record = { temp: 6, vms: 5, cts: 5, - cluster: 13, + cluster: 12, }; const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record< @@ -69,13 +69,15 @@ const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record< disk: 15, uptime: 15, }, + // Phones carry the same five columns as the guest table below them. A sixth + // (temperature) left the node name with four characters, which made a + // cluster of "Analytics A".."F" rows unreadable. phone: { node: 32, - cpu: 13, - memory: 13, - disk: 13, - temp: 15, - uptime: 14, + cpu: 15, + memory: 15, + disk: 15, + uptime: 15, }, mobile: { node: 32, @@ -146,6 +148,11 @@ export const getProxmoxHostVisibleColumnsForLayout = ( ); }; +// The node cell carries the expansion toggle and status dot before the name, +// so it needs more than the shared 30% phone identity anchor to show a +// typical node name whole. +const PROXMOX_HOST_PHONE_IDENTITY_WIDTH_PERCENT = 38; + export const getProxmoxHostColumnWidthStyle = ( columnId: ProxmoxHostTableColumnId, layoutMode: WorkloadTableLayoutMode, @@ -159,9 +166,11 @@ export const getProxmoxHostColumnWidthStyle = ( visibleColumnIds, layoutMode === 'narrow' ? { columnId: 'node', widthPercent: PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT } - : layoutMode === 'phone' || layoutMode === 'mobile' - ? { columnId: 'node', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } - : undefined, + : layoutMode === 'phone' + ? { columnId: 'node', widthPercent: PROXMOX_HOST_PHONE_IDENTITY_WIDTH_PERCENT } + : layoutMode === 'mobile' + ? { columnId: 'node', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } + : undefined, ); };