Name the archive format in By date and widen phone guest names

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
This commit is contained in:
rcourtman
2026-10-02 23:35:55 +01:00
parent 852ebdca08
commit 083cd070fe
8 changed files with 95 additions and 38 deletions
@@ -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
+17 -23
View File
@@ -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"
]
}
@@ -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' });
});
@@ -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;
};
@@ -730,7 +730,7 @@ export function ProxmoxCoverageTable(props: {
>
<span
class="inline-block max-w-[24rem] truncate"
title={artifact.detail}
title={artifact.detailTitle ?? artifact.detail}
>
{artifact.detail || '—'}
</span>
@@ -192,7 +192,10 @@ export function ProxmoxRecoverableTable(props: {
</TableCell>
<Show when={columnVisible('details')}>
<TableCell class={`${getPlatformTableCellClassForKind('text')} text-base-content`}>
<span class="inline-block max-w-[20rem] truncate" title={artifact.detail}>
<span
class="inline-block max-w-[20rem] truncate"
title={artifact.detailTitle ?? artifact.detail}
>
{artifact.detail || '—'}
</span>
</TableCell>
@@ -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({})],
@@ -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