From 083cd070fe0da5a473d9c7cb3366eab163c984ff Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:35:55 +0100 Subject: [PATCH] Name the archive format in By date and widen phone guest names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The By date table's Details cell showed a PVE backup's full volid, "backup-vault:backup/vzdump-qemu-1002-2026_10_02-20_10_14.vma.zst", which repeats the storage, guest type, VMID, and timestamp the row already shows in their own columns and clipped in every one of its rows on a 1440px screen. The one fact it adds is the archive format, so the cell now reads "vma.zst" or "tar.zst", the volid stays on hover and in search, and a provider with no format falls back to the file name. The coverage disclosure's evidence table follows suit. The shared guest table's phone name cell carries the expansion toggle, status dot, and backup badge before the name, so the shared 30% identity anchor left a 390px phone 66px for the name and clipped "artifact-cache-310" to "artifact-ca…". It now takes the same 38% anchor the Proxmox host table uses, which gives the name 95px while the four metric chips beside it still fit. Measured with a scrollWidth-over-clientWidth sweep at 1440px, 560px and 390px: the By date table has no clipped cell at desktop width, and on phones only long identity strings still clip. Contract-Neutral: By date Details cell and guest-table phone identity width are presentation only; unified-resources note updated; no storage-recovery authority, workloads hot-path, or frontend-primitives change --- .../internal/subsystems/unified-resources.md | 5 +++ frontend-modern/browser-verification.json | 40 ++++++++----------- .../Workloads/__tests__/GuestRow.test.tsx | 21 +++++----- .../components/Workloads/guestRowModel.tsx | 15 +++++-- .../features/proxmox/ProxmoxCoverageTable.tsx | 2 +- .../proxmox/ProxmoxRecoverableTable.tsx | 5 ++- .../proxmoxBackupRecoveryModel.test.ts | 29 ++++++++++++++ .../proxmox/proxmoxBackupRecoveryModel.ts | 16 +++++++- 8 files changed, 95 insertions(+), 38 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 7ae2bf6b82..8106739eb7 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -854,6 +854,11 @@ 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. +The shared guest table's phone name cell carries the same leading toggle, +status dot, and backup badge, so it takes the same wider phone anchor. The +recoverable row's Details cell names the PVE archive format rather than the +volid, which repeats the storage, guest type, VMID, and timestamp the row +already shows in their own columns; the volid stays on hover and in search. 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 6b556a231e..c5a89dd2c3 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,28 +1,24 @@ { "version": 1, - "base_sha": "3f2715e3f3f9b563ad3f119ba0d302b488b97e8a", - "verified_at": "2026-10-02T22:20:55Z", + "base_sha": "852ebdca080535c10b2d10dd39465953cd47b8ef", + "verified_at": "2026-10-02T22:35:55Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", + "frontend-modern/src/components/Workloads/guestRowModel.tsx", "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", "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" + "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts" ], "content_sha256": { - "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" + "frontend-modern/src/components/Workloads/guestRowModel.tsx": "7787854145257e13bc347c2513f89e595060e606857a9a7c4ae6c0a897b6c639", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "6a546a5c1e8a9bbe6a2bdeac61c988a8e9f35cf96888adff245a5efd7017769e", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx": "fdc9fc2720ed04771f5c9d271f8972a4dece25e165064183fcf59678466a970e", + "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts": "b872437fb4cfb61b07e0c949ae6f323d6fd3f2622e57480931f7ef74fafe79c2" }, "routes": [ "/proxmox/overview", - "/proxmox/backups/coverage", - "/proxmox/backups/date" + "/proxmox/backups/date", + "/proxmox/backups/coverage" ], "viewports": [ { @@ -39,16 +35,14 @@ } ], "states": [ - "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" + "Proxmox guest table at 390px: name cell widened from 66px to 95px of text so typical guest names render whole; CPU, memory, disk and age chips still fit", + "By date table at 1440px: Details cells show the archive format (vma.zst, tar.zst) or a PBS file count; the volid is the hover title; zero clipped cells where 136 clipped before", + "By date table at 560px and 390px: Details column hidden by layout as before; only datastore identity strings clip", + "Coverage restore-evidence disclosure: Details shows the archive format with the volid on hover" ], "interactions": [ - "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" + "scrollWidth-over-clientWidth sweep of every table cell on each route at the three widths", + "Measured the guest table column widths and name cell parts through the DOM at 390px, 360px and 430px", + "Scrolled the guests section and the By date table into view for screenshots" ] } diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx index 6e72824233..d6fb813c6b 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx @@ -1308,23 +1308,26 @@ describe('GUEST_COLUMNS', () => { }); it('derives mobile overrides from the canonical guest column model', () => { + // The phone name cell carries the toggle, status dot, and backup badge, so + // it takes a wider anchor than the shared 30% and the metric chips share + // the rest. expect(getGuestColumnStyle('name', true)).toEqual({ - width: '30%', - 'max-width': '30%', + width: '38%', + 'max-width': '38%', }); expect(getGuestColumnStyle('cpu', true)).toEqual({ - width: '11.3235%', - 'max-width': '11.3235%', + width: '10.0294%', + 'max-width': '10.0294%', }); expect(getGuestColumnStyle('availability', true)).toEqual({ - width: '7.2059%', - 'max-width': '7.2059%', + width: '6.3824%', + 'max-width': '6.3824%', }); expect(getGuestColumnStyle('type', true)).toEqual({ - width: '9.2647%', - 'max-width': '9.2647%', + width: '8.2059%', + 'max-width': '8.2059%', }); - expect(getGuestColumnWidthStyle('name', true)).toEqual({ width: '30%' }); + expect(getGuestColumnWidthStyle('name', true)).toEqual({ width: '38%' }); expect(getGuestColumnWidthStyle('diskIo', true)).toEqual({ width: '170px' }); }); diff --git a/frontend-modern/src/components/Workloads/guestRowModel.tsx b/frontend-modern/src/components/Workloads/guestRowModel.tsx index 40b81bf47e..0c7c67ea17 100644 --- a/frontend-modern/src/components/Workloads/guestRowModel.tsx +++ b/frontend-modern/src/components/Workloads/guestRowModel.tsx @@ -469,6 +469,13 @@ const percentageColumn = (width: string): GuestColumnWidthOverride => ({ maxWidth: width, }); +// The phone name cell carries the expansion toggle, status dot, and backup +// badge before the name, so the shared 30% identity anchor left a 390px phone +// 66px for the name and clipped "artifact-cache-310" to "artifact-ca…". The +// four metric chips beside it need far less than an even split, so the name +// takes the same wider phone anchor the Proxmox host table uses. +const GUEST_PHONE_IDENTITY_WIDTH_PERCENT = 38; + // Responsive weights are normalized against the currently visible column set. // That keeps each workload view mode full-width without assuming one fixed set. const GUEST_COLUMN_RESPONSIVE_WEIGHTS: Record< @@ -548,9 +555,11 @@ const getResponsiveColumnOverride = ( activeIds, layoutMode === 'narrow' ? { columnId: 'name', widthPercent: PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT } - : layoutMode === 'phone' || layoutMode === 'mobile' - ? { columnId: 'name', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } - : undefined, + : layoutMode === 'phone' + ? { columnId: 'name', widthPercent: GUEST_PHONE_IDENTITY_WIDTH_PERCENT } + : layoutMode === 'mobile' + ? { columnId: 'name', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } + : undefined, ); return typeof style.width === 'string' ? percentageColumn(style.width) : undefined; }; diff --git a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx index 87bc2eb4ce..5c58e4c8f7 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx @@ -730,7 +730,7 @@ export function ProxmoxCoverageTable(props: { > {artifact.detail || '—'} diff --git a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx index 6457d00b66..4c29fd190c 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx @@ -192,7 +192,10 @@ export function ProxmoxRecoverableTable(props: { - + {artifact.detail || '—'} diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts index 0c5ff7f957..43fc7e5a41 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts @@ -162,6 +162,35 @@ describe('proxmoxBackupRecoveryModel', () => { ); }); + it('shows the archive format as its detail and keeps the volid on hover and in search', () => { + const model = buildProxmoxBackupRecoveryModel({ + workloads: [workload({})], + pbsBackups: [], + archives: [archive({ format: 'tar.zst' })], + snapshots: [], + tasks: [], + nowMs: Date.parse('2026-05-26T08:00:00Z'), + }); + + const [artifact] = model.recoverableArtifacts; + expect(artifact.detail).toBe('tar.zst'); + expect(artifact.detailTitle).toBe('local:backup/vzdump-lxc-112-2026_05_24-02_00_00.tar.zst'); + expect(recoverableArtifactMatchesSearch(artifact, 'vzdump-lxc-112')).toBe(true); + }); + + it('falls back to the archive file name when the provider reports no format', () => { + const model = buildProxmoxBackupRecoveryModel({ + workloads: [workload({})], + pbsBackups: [], + archives: [archive({ format: '' })], + snapshots: [], + tasks: [], + nowMs: Date.parse('2026-05-26T08:00:00Z'), + }); + + expect(model.recoverableArtifacts[0].detail).toBe('vzdump-lxc-112-2026_05_24-02_00_00.tar.zst'); + }); + it('keeps a terminally incomplete PBS artifact out of latest recovery', () => { const model = buildProxmoxBackupRecoveryModel({ workloads: [workload({})], diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts index 2fba96c60a..b4288cf9f6 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts @@ -47,6 +47,8 @@ export interface RecoverableArtifact { /** Operator-facing source/repository label used by the backup-location filter. */ locationLabel?: string; detail: string; + /** Full provider identifier behind a shortened `detail`, shown on hover. */ + detailTitle?: string; protected: boolean; verified?: boolean; /** A still-running backup is writing this artifact; it is not recoverable yet. */ @@ -157,6 +159,12 @@ function parseTimestampMs(value: string | undefined): number | undefined { return Number.isFinite(ms) ? ms : undefined; } +function archiveFileName(volid: string | undefined): string { + const trimmed = volid?.trim() ?? ''; + if (!trimmed) return ''; + return trimmed.slice(trimmed.lastIndexOf('/') + 1); +} + function pbsBackupFileDetailLabel(fileCount: number): string { if (fileCount <= 0) return 'PBS files not listed'; if (fileCount === 1) return '1 PBS file'; @@ -585,7 +593,12 @@ export function buildProxmoxBackupRecoveryModel( size: archive.size, location: archive.storage || archive.node || '—', ...artifactLocationScope('archive', archive.instance, archive.storage || archive.node), - detail: archive.volid || archive.format || archiveSource.detailFallbackLabel, + // The volid repeats the storage, guest type, VMID, and timestamp the row + // already shows in their own columns and clips at every width; the + // archive format is the one fact it adds, so that is the cell and the + // volid stays on hover and in search. + detail: archive.format || archiveFileName(archive.volid) || archiveSource.detailFallbackLabel, + detailTitle: archive.volid || undefined, protected: archive.protected, verified: archive.inProgress ? undefined : archive.isPBS ? archive.verified : undefined, running: archive.inProgress === true, @@ -743,6 +756,7 @@ export function recoverableArtifactMatchesSearch( artifact.sourceTitle, artifact.location, artifact.detail, + artifact.detailTitle, artifact.verified === true ? 'verified' : artifact.verified === false