diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
index 23f425ce9..936f37438 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -299,6 +299,10 @@ regression protection.
namespace reference in tooltip/detail metadata, and the image column must
stay left-aligned as text; table rows should optimize for
container/version scanning, not registry path inspection.
+ Workload backup cells may expose a compact visible freshness label derived
+ from the row's scalar `lastBackup` value, but they must remain
+ presentation-only: no per-row backup API calls, recovery evidence joins, or
+ provider-specific backup fetches belong in the shared workload hot path.
11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx`
Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`.
History cards must let the plot area stretch to the card height instead of
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index a6a0fd051..f87c35817 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -103,6 +103,12 @@ into PVE archive or snapshot semantics. Workload coverage posture must be
derived from real recovery evidence and recent task outcomes, including
explicit uncovered and failed-latest-task states, rather than from source-detail
row counts alone.
+The Proxmox overview workload table's Backup column is the primary
+glance-level protection signal for current guests. It must remain visible,
+dense, and backed by the canonical guest `LastBackup` synchronization from PVE
+storage and PBS evidence; the Proxmox Backups tab is the drilldown for
+coverage, restore-point, and source-detail evidence, not the first place users
+must visit to learn whether a guest is backed up.
The platform page embedding point may pass read-only Proxmox guest inventory
into that backup surface solely for workload identity correlation; the backup
surface must still source restore evidence from the PVE/PBS backup APIs rather
diff --git a/frontend-modern/src/components/Workloads/GuestRowCells.tsx b/frontend-modern/src/components/Workloads/GuestRowCells.tsx
index 34b196c39..ab1c3b923 100644
--- a/frontend-modern/src/components/Workloads/GuestRowCells.tsx
+++ b/frontend-modern/src/components/Workloads/GuestRowCells.tsx
@@ -4,7 +4,7 @@ import { TooltipPortal } from '@/components/shared/TooltipPortal';
import { useTooltip } from '@/hooks/useTooltip';
import { useAlertsActivation } from '@/stores/alertsActivation';
import type { GuestNetworkInterface } from '@/types/api';
-import { formatBytes, getBackupInfo } from '@/utils/format';
+import { formatBytes, formatRelativeTime, getBackupInfo, type BackupInfo } from '@/utils/format';
import {
getWorkloadsGuestBackupStatusPresentation,
getWorkloadsGuestBackupTooltip,
@@ -71,6 +71,35 @@ function BackupIndicator(props: {
);
}
+function getBackupAgeBadgeLabel(
+ lastBackup: string | number | null | undefined,
+ info: BackupInfo,
+): string {
+ if (info.status === 'never') return 'None';
+ const compact = formatRelativeTime(lastBackup ?? undefined, {
+ compact: true,
+ emptyText: 'Unknown',
+ });
+ if (compact === 'just now') return 'Now';
+ return compact.replace(/\s+ago$/, '');
+}
+
+function getBackupAgeBadgeClass(status: BackupInfo['status']): string {
+ const base =
+ 'inline-flex h-5 min-w-[3.25rem] items-center justify-center gap-1 rounded-full border px-1.5 text-[10px] font-semibold leading-none tabular-nums cursor-help';
+
+ switch (status) {
+ case 'fresh':
+ return `${base} border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-950/40 dark:text-green-300`;
+ case 'stale':
+ return `${base} border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/70 dark:bg-yellow-950/40 dark:text-yellow-300`;
+ case 'critical':
+ return `${base} border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300`;
+ case 'never':
+ return `${base} border-border bg-surface-alt text-muted`;
+ }
+}
+
function NetworkInfoCell(props: {
ipAddresses: string[];
networkInterfaces: GuestNetworkInterface[];
@@ -276,17 +305,23 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
getBackupInfo(props.lastBackup, alertsActivation.getBackupThresholds()),
);
const config = createMemo(() => getWorkloadsGuestBackupStatusPresentation(info().status));
+ const badgeLabel = createMemo(() => getBackupAgeBadgeLabel(props.lastBackup, info()));
+ const ariaLabel = createMemo(() => {
+ const currentInfo = info();
+ if (currentInfo.status === 'never') return 'Backup status: never';
+ return `Backup status: ${currentInfo.status}, last backup ${currentInfo.ageFormatted}`;
+ });
return (
<>
+ {badgeLabel()}
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx
index 9a2bdd404..6cc1a1e1f 100644
--- a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx
+++ b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx
@@ -187,6 +187,7 @@ function renderGuestRow(props: Parameters[0]) {
afterEach(() => {
cleanup();
+ vi.useRealTimers();
vi.clearAllMocks();
isMobileMock.mockReturnValue(false);
});
@@ -1172,6 +1173,33 @@ describe('context column for PVE workloads', () => {
});
describe('backup column', () => {
+ it('shows compact age text for supported guests with a backup', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-05-26T12:00:00Z'));
+
+ const { container } = renderGuestRow({
+ guest: makeGuest({
+ type: 'qemu',
+ workloadType: 'vm',
+ lastBackup: Date.parse('2026-05-26T07:00:00Z'),
+ }),
+ visibleColumnIds: ['name', 'backup'],
+ });
+
+ expect(screen.getByText('5h')).toBeTruthy();
+ expect(
+ container.querySelector('[aria-label="Backup status: fresh, last backup 5 hours ago"]'),
+ ).toBeTruthy();
+ });
+
+ it('shows None for supported guests without a backup', () => {
+ renderGuestRow({
+ guest: makeGuest({ type: 'qemu', workloadType: 'vm', lastBackup: 0 }),
+ visibleColumnIds: ['name', 'backup'],
+ });
+ expect(screen.getByText('None')).toBeTruthy();
+ });
+
it('shows dash for app-container workloads (no backup support)', () => {
renderGuestRow({
guest: makeGuest({ type: 'app-container', workloadType: 'app-container' }),
diff --git a/frontend-modern/src/components/Workloads/guestRowModel.tsx b/frontend-modern/src/components/Workloads/guestRowModel.tsx
index 89b103bee..d080cba97 100644
--- a/frontend-modern/src/components/Workloads/guestRowModel.tsx
+++ b/frontend-modern/src/components/Workloads/guestRowModel.tsx
@@ -303,7 +303,8 @@ export const GUEST_COLUMNS: ColumnDef[] = [
/>
),
- width: '50px',
+ width: '72px',
+ minWidth: '68px',
toggleable: true,
kind: 'badge',
},