diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 679ce24ef..e961edd4e 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -3905,7 +3905,9 @@ after applying a view gives someone else the exact filtered state.
Counts or orientation strips presented as part of a filtered resource table
must derive from that table's canonical filtered collection as well. They must
not continue showing page-wide inventory totals after FilterBar state, saved
-views, or search has narrowed the rows the operator can see.
+views, or search has narrowed the rows the operator can see. Filtered summary
+strips should omit zero-value categories and their separators rather than
+turning absent states into persistent visual noise.
Implicit "remember last filters" is intentionally not added — defaulting
to yesterday's filter state on a monitoring page hides real problems.
That same shared filter-toolbar boundary also owns controlled select continuity
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 5346867ba..7ff6a15c4 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -272,6 +272,12 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
seed a missing `status` parameter when a platform page is revisited, but the
restore path must write the URL once with `replace` and must not force row
filtering through a separate page-local state channel.
+ Status buckets must remain mutually exclusive and derive from the already-
+ loaded workload snapshot: warning or offline unified-resource health takes
+ precedence for filter/stats posture, while healthy resources continue to use
+ the provider-authored runtime power state. `useWorkloads.ts` carries both
+ values; selectors must not collapse warning health into `Stopped` or add a
+ second resource lookup to recover it.
4. Keep shared auth gating in `internal/api/router.go` cheap and local: pre-auth quick-setup and recovery routing may short-circuit on loopback/session/token checks, but they must not trigger chart, metrics, or broad persistence fan-out on the protected request hot path.
Agent command authorization is likewise a dispatch-time point lookup and
atomic approval consume, not a route-wide scan or request-hot-path fan-out;
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 7e8cbc7e6..cdb0b542f 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -472,7 +472,9 @@ The adjacent Proxmox overview guest-totals strip is likewise presentation-only
and must consume the embedded Workloads surface's filtered statistics. It must
not fall back to page-wide guest summary counts when search, type, status, or
node filters have narrowed the visible workload inventory; backup and recovery
-authority remain unchanged by either set of presentation counts.
+authority remain unchanged by either set of presentation counts. Filtered
+totals omit zero-value status categories and their separators so the strip
+does not add false orientation noise beside a narrowed result set.
That overview Backup signal belongs to Proxmox VMs and LXCs. If the embedded
Workloads table demotes Docker-in-LXC `app-container` rows out of peer
membership, the backup surface must still receive the page model's Proxmox
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index f04dd39c9..41a8fc3a0 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,13 +1,19 @@
{
"version": 1,
- "base_sha": "c143a7cf322b407e8091ca4e27a16e92a5178cef",
- "verified_at": "2026-08-02T20:53:27Z",
+ "base_sha": "bacff3e2ebbb53dfa54881132d971e11e50d452e",
+ "verified_at": "2026-08-02T21:27:13Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
+ "frontend-modern/src/components/Workloads/workloadSelectors.ts",
+ "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx",
+ "frontend-modern/src/hooks/useWorkloads.ts",
+ "frontend-modern/src/types/workloads.ts"
],
"content_sha256": {
- "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "3dafdf4fec6e6843082f347258c7913f96bc1bf9fc23b10e2461d87ffa90110f"
+ "frontend-modern/src/components/Workloads/workloadSelectors.ts": "bca56f3abac29d36ed30e361747f4b32138bd0fd75112ff04aed8e2ca15dc01d",
+ "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "a26664090d776ac8c99bfd14c52fafa3157cf8aebf275807b2a65588664b1fe7",
+ "frontend-modern/src/hooks/useWorkloads.ts": "f6064a2dd39d50d2c24c91f7499e21bcb4f115f9e99ae90420536c8e6834b06a",
+ "frontend-modern/src/types/workloads.ts": "52996a1fa0b08da1167770522302053f72da4f9d40ec3b0cd17b0dd9b1c91749"
},
"routes": [
"/proxmox/overview"
@@ -23,17 +29,17 @@
}
],
"states": [
- "Desktop filtered saved view with one stopped VM",
- "Phone-width filtered saved view with one stopped VM",
- "Desktop zero-result Attention filter",
- "Desktop unfiltered workload inventory",
- "Clean unfiltered state after saved-view removal and reload"
+ "Desktop unfiltered Proxmox workload inventory",
+ "Desktop Attention filter with 11 warning-health guests",
+ "Desktop Stopped filter with 5 stopped-health guests",
+ "Phone-width expanded filters with Attention selected",
+ "Clean unfiltered state after clearing filters and reload"
],
"interactions": [
- "Confirmed the one-row VMs, Stopped, and Node pve1 view rendered 0 running and 1 stopped instead of estate-wide totals",
- "Inspected the filtered totals strip at 1280x900 and 390x844 for alignment, clipping, and horizontal overflow",
- "Cleared the saved filters and confirmed the totals returned to the full visible workload collection",
- "Applied the Attention filter and confirmed the totals strip was absent beside the canonical zero-result state",
- "Removed the temporary saved view, cleared filters, reloaded the route, and confirmed no saved default or filter state remained"
+ "Applied Attention and confirmed the 11 node-offline warning guests rendered instead of an empty result",
+ "Applied Stopped and confirmed only the 5 genuinely stopped guests remained",
+ "Confirmed filtered totals omitted zero-value categories and stray separators",
+ "Expanded the phone-width filter controls, selected Attention, and inspected table alignment and viewport clipping",
+ "Cleared filters, reloaded the route, and confirmed the unfiltered totals restored to 53 running, 11 attention, and 5 stopped"
]
}
diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov0712b.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov0712b.test.ts
index ef3e6477d..8de5ae9e0 100644
--- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov0712b.test.ts
+++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov0712b.test.ts
@@ -677,14 +677,14 @@ describe('workloadSelectors (branch coverage 2)', () => {
).toEqual(['c1', 'c2']);
});
- it('matches status exactly (case-sensitive) in running mode', () => {
+ it('matches running status case-insensitively', () => {
const guests = [
makeGuest(1, { id: 'lower', status: 'running' }),
makeGuest(2, { id: 'capital', status: 'Running' }),
];
expect(
filterWorkloads({ ...baseFilterParams, guests, statusMode: 'running' }).map((g) => g.id),
- ).toEqual(['lower']);
+ ).toEqual(['lower', 'capital']);
});
it('counts DEGRADED-set and unknown statuses as degraded, excluding running and OFFLINE-set', () => {
@@ -700,16 +700,15 @@ describe('workloadSelectors (branch coverage 2)', () => {
).toEqual(['warn', 'migrating', 'empty']);
});
- it('treats capitalized "Running" as stopped due to the case-sensitive !== running check', () => {
+ it('normalizes status casing before assigning mutually exclusive buckets', () => {
const guests = [
makeGuest(1, { id: 'capital', status: 'Running' }),
makeGuest(2, { id: 'lower', status: 'running' }),
makeGuest(3, { id: 'stopped', status: 'stopped' }),
];
- // stopped mode keeps g.status !== 'running' -> 'Running' survives (case quirk).
expect(
filterWorkloads({ ...baseFilterParams, guests, statusMode: 'stopped' }).map((g) => g.id),
- ).toEqual(['capital', 'stopped']);
+ ).toEqual(['stopped']);
});
it('applies no status filter for an unrecognized statusMode (else arm)', () => {
diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov2.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov2.test.ts
index 8db88f0bd..e5c470d2d 100644
--- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov2.test.ts
+++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov2.test.ts
@@ -261,7 +261,7 @@ describe('workloadSelectors (branch coverage 2)', () => {
expect(result.map((g) => g.id)).toEqual(['empty']);
});
- it('matches status case-sensitively in running mode (capital "Running" is excluded)', () => {
+ it('normalizes status casing in running mode', () => {
const guests = [
makeGuest(1, { id: 'capital', name: 'capital', status: 'Running' }),
makeGuest(2, { id: 'lower', name: 'lower', status: 'running' }),
@@ -273,8 +273,7 @@ describe('workloadSelectors (branch coverage 2)', () => {
statusMode: 'running',
});
- // g.status === 'running' is an exact, case-sensitive comparison.
- expect(result.map((g) => g.id)).toEqual(['lower']);
+ expect(result.map((g) => g.id)).toEqual(['capital', 'lower']);
});
it('matches the numeric vmid and string status/instance text-search candidates', () => {
@@ -409,7 +408,7 @@ describe('workloadSelectors (branch coverage 2)', () => {
});
});
- it('classifies a capitalized "Running" status as stopped due to case-sensitive running check', () => {
+ it('classifies a capitalized "Running" status as running', () => {
const guests = [
makeGuest(1, {
id: 'capital',
@@ -420,13 +419,11 @@ describe('workloadSelectors (branch coverage 2)', () => {
}),
];
- // g.status === 'running' is exact; the degraded check lowercases, so
- // 'Running' is neither running nor degraded and lands in stopped.
expect(computeWorkloadStats(guests)).toStrictEqual({
total: 1,
- running: 0,
+ running: 1,
degraded: 0,
- stopped: 1,
+ stopped: 0,
vms: 1,
containers: 0,
appContainers: 0,
diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts
index abe784cc6..9521567c0 100644
--- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts
+++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts
@@ -124,7 +124,40 @@ describe('workloadSelectors', () => {
selectedHostHint: null,
selectedKubernetesContext: null,
});
- expect(stopped.map((g) => g.status)).toEqual(['warning', 'migrating', 'offline']);
+ expect(stopped.map((g) => g.status)).toEqual(['offline']);
+ });
+
+ it('lets canonical resource health override stale runtime power for status filters', () => {
+ const guests = [
+ makeGuest(1, { status: 'stopped', resourceStatus: 'warning' }),
+ makeGuest(2, { status: 'running', resourceStatus: 'offline' }),
+ makeGuest(3, { status: 'running', resourceStatus: 'online' }),
+ ];
+
+ const filterByStatus = (statusMode: 'running' | 'degraded' | 'stopped') =>
+ filterWorkloads({
+ guests,
+ viewMode: 'all',
+ statusMode,
+ searchTerm: '',
+ selectedNode: null,
+ selectedHostHint: null,
+ selectedKubernetesContext: null,
+ }).map((guest) => guest.id);
+
+ expect(filterByStatus('running')).toEqual([guests[2].id]);
+ expect(filterByStatus('degraded')).toEqual([guests[0].id]);
+ expect(filterByStatus('stopped')).toEqual([guests[1].id]);
+ expect(computeWorkloadStats(guests)).toEqual({
+ total: 3,
+ running: 1,
+ degraded: 1,
+ stopped: 1,
+ vms: 2,
+ containers: 0,
+ appContainers: 1,
+ pods: 0,
+ });
});
it('applies text and metric search filters, and supports combined filtering', () => {
diff --git a/frontend-modern/src/components/Workloads/workloadSelectors.ts b/frontend-modern/src/components/Workloads/workloadSelectors.ts
index 3f7c1220e..83a352c90 100644
--- a/frontend-modern/src/components/Workloads/workloadSelectors.ts
+++ b/frontend-modern/src/components/Workloads/workloadSelectors.ts
@@ -5,7 +5,7 @@ import { computeIOScale } from '@/components/Infrastructure/infrastructureSelect
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { parseFilterStack, evaluateFilterStack, splitSearchExclusions } from '@/utils/searchQuery';
import { normalizeSourcePlatformQueryValue } from '@/utils/sourcePlatforms';
-import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES } from '@/utils/status';
+import { OFFLINE_HEALTH_STATUSES } from '@/utils/status';
import { getNodeDisplayName } from '@/utils/nodes';
import {
isContainerWorkloadViewMode,
@@ -48,6 +48,29 @@ export interface WorkloadStats {
pods: number;
}
+type WorkloadStatusBucket = 'running' | 'degraded' | 'stopped';
+
+const RUNNING_HEALTH_STATUSES = new Set(['online', 'running', 'healthy']);
+
+const classifyWorkloadStatus = (status: string): WorkloadStatusBucket => {
+ const normalized = status.trim().toLowerCase();
+ if (RUNNING_HEALTH_STATUSES.has(normalized)) return 'running';
+ if (OFFLINE_HEALTH_STATUSES.has(normalized)) return 'stopped';
+ return 'degraded';
+};
+
+const resolveWorkloadStatusBucket = (guest: WorkloadGuest): WorkloadStatusBucket => {
+ const resourceStatus = (guest.resourceStatus || '').trim().toLowerCase();
+
+ // Unified-resource health owns warning/offline posture, while a healthy
+ // resource still defers to the provider-authored runtime power state.
+ if (resourceStatus && !RUNNING_HEALTH_STATUSES.has(resourceStatus)) {
+ return classifyWorkloadStatus(resourceStatus);
+ }
+
+ return classifyWorkloadStatus(guest.status || '');
+};
+
type SortDirection = 'asc' | 'desc';
type SortValue = string | number | boolean | null | undefined;
@@ -152,17 +175,11 @@ export const filterWorkloads = ({
}
if (statusMode === 'running') {
- guests = guests.filter((g) => g.status === 'running');
+ guests = guests.filter((g) => resolveWorkloadStatusBucket(g) === 'running');
} else if (statusMode === 'degraded') {
- guests = guests.filter((g) => {
- const status = (g.status || '').toLowerCase();
- return (
- DEGRADED_HEALTH_STATUSES.has(status) ||
- (status !== 'running' && !OFFLINE_HEALTH_STATUSES.has(status))
- );
- });
+ guests = guests.filter((g) => resolveWorkloadStatusBucket(g) === 'degraded');
} else if (statusMode === 'stopped') {
- guests = guests.filter((g) => g.status !== 'running');
+ guests = guests.filter((g) => resolveWorkloadStatusBucket(g) === 'stopped');
}
const trimmedSearch = searchTerm.trim();
@@ -437,15 +454,15 @@ export const buildWorkloadSummaryGroupScopeMap = ({
};
export const computeWorkloadStats = (guests: WorkloadGuest[]): WorkloadStats => {
- const running = guests.filter((g) => g.status === 'running').length;
- const degraded = guests.filter((g) => {
- const status = (g.status || '').toLowerCase();
- return (
- DEGRADED_HEALTH_STATUSES.has(status) ||
- (status !== 'running' && !OFFLINE_HEALTH_STATUSES.has(status))
- );
- }).length;
- const stopped = guests.length - running - degraded;
+ let running = 0;
+ let degraded = 0;
+ let stopped = 0;
+ guests.forEach((guest) => {
+ const bucket = resolveWorkloadStatusBucket(guest);
+ if (bucket === 'running') running += 1;
+ else if (bucket === 'degraded') degraded += 1;
+ else stopped += 1;
+ });
const vms = guests.filter((g) => resolveWorkloadType(g) === 'vm').length;
const containers = guests.filter((g) => resolveWorkloadType(g) === 'system-container').length;
const appContainers = guests.filter((g) => resolveWorkloadType(g) === 'app-container').length;
diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
index 01b1cee6f..56a49e9c2 100644
--- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
+++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
@@ -381,22 +381,30 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
class="flex items-center gap-2 rounded border border-border bg-surface-alt px-2 py-1 text-xs text-muted"
data-testid="proxmox-guest-totals"
>
-
-
- {visibleGuestStats().running} running
-
+ 0}>
+
+
+ {visibleGuestStats().running} running
+
+
0}>
- |
+ 0}>
+ |
+
{visibleGuestStats().degraded} attention
- |
-
-
- {visibleGuestStats().stopped} stopped
-
+ 0}>
+ 0 || visibleGuestStats().degraded > 0}>
+ |
+
+
+
+ {visibleGuestStats().stopped} stopped
+
+
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
index 52934e17e..1a158a9ec 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
@@ -387,6 +387,12 @@ describe('ProxmoxBackupsTable', () => {
expect(proxmoxPageSurfaceSource).not.toContain(
'currentModel().summary.stoppedGuestCount} stopped',
);
+ expect(proxmoxPageSurfaceSource).toContain(
+ ' 0}>',
+ );
+ expect(proxmoxPageSurfaceSource).toContain(
+ ' 0}>',
+ );
});
it('keeps the shared storage surface scoped to the whole Proxmox product family', () => {
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
index 973e60099..12aae6e24 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
@@ -238,7 +238,7 @@ describe('ProxmoxPageSurface contract', () => {
render(() => );
const totals = screen.getByTestId('proxmox-guest-totals');
- expect(totals).toHaveTextContent('0 running');
+ expect(totals).not.toHaveTextContent('running');
expect(totals).not.toHaveTextContent('attention');
expect(totals).toHaveTextContent('1 stopped');
});
diff --git a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
index fcff584c1..db4e8e34c 100644
--- a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
+++ b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
@@ -276,7 +276,7 @@ describe('useWorkloads', () => {
dispose();
});
- it('keeps Proxmox power state stable while aggregate freshness changes, then removes an authoritative deletion', async () => {
+ it('keeps Proxmox power state stable while carrying aggregate health, then removes an authoritative deletion', async () => {
const guest = (vmid: number, status: string, runtimeStatus: string) => ({
...sampleResource,
id: `cluster-a-pve1-${vmid}`,
@@ -331,6 +331,11 @@ describe('useWorkloads', () => {
'running',
'running',
]);
+ expect(result!.workloads().map((workload) => workload.resourceStatus)).toEqual([
+ 'warning',
+ 'online',
+ 'warning',
+ ]);
apiFetchJSONMock.mockResolvedValueOnce({
data: [guest(101, 'online', 'running'), guest(102, 'online', 'running')],
diff --git a/frontend-modern/src/hooks/useWorkloads.ts b/frontend-modern/src/hooks/useWorkloads.ts
index 122f8e08c..e17de7129 100644
--- a/frontend-modern/src/hooks/useWorkloads.ts
+++ b/frontend-modern/src/hooks/useWorkloads.ts
@@ -462,6 +462,7 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => {
resource.status ||
(platformType === 'vmware-vsphere' ? resource.vmware?.powerState : null),
),
+ resourceStatus: (resource.status || '').trim().toLowerCase() || undefined,
type:
workloadType === 'vm'
? 'vm'
diff --git a/frontend-modern/src/types/workloads.ts b/frontend-modern/src/types/workloads.ts
index daa3f19ec..b1237200a 100644
--- a/frontend-modern/src/types/workloads.ts
+++ b/frontend-modern/src/types/workloads.ts
@@ -12,6 +12,8 @@ export type ViewMode = 'all' | 'vm' | WorkloadContainerViewMode | 'pod';
export type WorkloadGuest = (VM | Container) & {
workloadType?: WorkloadType;
+ /** Canonical unified-resource health, kept separate from runtime power state in `status`. */
+ resourceStatus?: string;
displayId?: string;
image?: string;
// Provider/runtime-native identifier for app-container actions such as Docker image updates.