diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 85c281c011..6c07a49441 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -2911,9 +2911,9 @@ Agent`), with the plain-language source phrase available through accessible
`pveVersion` or a Pulse Agent report whose OS identity resolves to Unraid or
Proxmox VE. They must omit the version rather than showing unrelated
collector OS versions, such as Debian 12, beside an API-backed PVE badge.
- Shared row primitives that render Proxmox node identity, including
- `frontend-modern/src/components/shared/NodeGroupHeader.tsx`, must route raw
- PVE manager payloads through
+ Surfaces that render Proxmox node versions, including the Proxmox page
+ model at `frontend-modern/src/features/proxmox/proxmoxPageModel.ts`, must
+ route raw PVE manager payloads through
`frontend-modern/src/utils/proxmoxVersion.ts` rather than inlining
page-local parsing or falling back to unrelated agent OS versions.
System title metadata must apply the same identity rule: once the primary
@@ -4395,12 +4395,14 @@ The shared table chrome now allows `TableCardHeader` to expose a right-aligned
action slot, currently used by the Workloads/Proxmox metric display control.
That slot belongs to the table header band and must not reintroduce nested
cards or page-local toolbar wrappers inside `TableCard`. Proxmox host grouping
-also extends the shared `NodeGroupHeader` row pattern: host metrics may align
-with workload table columns, but the shared primitive owns the header/table
-shell boundary rather than platform pages copying their own card headers.
-Compact PVE version text in that header must come from the shared Proxmox
-version formatter so raw `pve-manager/...` payloads and platform-page host
-version cells stay consistent.
+also extends the shared `NodeGroupHeader` row pattern, which owns the
+header/table shell boundary rather than platform pages copying their own card
+headers. `frontend-modern/src/components/shared/NodeGroupHeader.tsx` renders
+node identity only (status dot, linked name, cluster and agent badges); in
+table-row mode that identity sits in one cell spanning the table. It carries no
+per-column cell renderer and no inline
+version, temperature, or uptime facts: host metrics and versions belong to the
+platform page's host table, so a group row must not duplicate them.
Mobile navigation now recognizes `proxmox` as a first-class platform tab in
the shared priority model so app-shell ordering remains centralized.
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 87517e7399..5231c21704 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -1948,22 +1948,20 @@ query persistence once per resource, or run on the steady-state alert hot path;
ancestor lookup remains bounded by canonical hierarchy depth for each active
alert actually evaluated.
-The embedded WorkloadsSurface exposes a `compactGroupHeaders` prop on
-`frontend-modern/src/components/Workloads/useWorkloadsState.ts` that
-platform pages owning their own hosts table (Proxmox overview today) set
-to strip per-host metric cells out of the `NodeGroupHeader` rows in
-grouped mode. The flag is threaded through
-`frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`,
-`frontend-modern/src/components/Workloads/WorkloadsTable.tsx`, and
-`frontend-modern/src/components/Workloads/WorkloadPanel.tsx`; when set,
-`WorkloadPanel` calls `NodeGroupHeader` without `columns` /
-`renderColumnCell` so the existing colspan layout renders just the
-status dot, linked node name, cluster badge, and agent badge — the
-duplicate CPU / Memory / Disk / uptime / temperature / version stats
-the top-of-page hosts table already owns are dropped. New embedded
-platform-page consumers that render their own hosts summary must set
-this flag; standalone Workloads surfaces (no top hosts table) keep the
-original verbose group rows by default.
+Grouped host rows in the embedded WorkloadsSurface are identity-only
+dividers. `frontend-modern/src/components/Workloads/WorkloadPanel.tsx`
+renders each grouped node through `NodeGroupHeader` as one cell spanning
+the table: status dot, linked node name, cluster badge, and agent badge.
+Per-host CPU, memory, disk, network and disk I/O, uptime, temperature,
+and version stats belong to the platform page's own hosts table (the
+Proxmox nodes table today), so the workloads table must not render them
+per column or inline in the group row. The former `compactGroupHeaders`
+option and the per-column node metric cells it switched off were removed
+because both remaining production mounts (Proxmox and vSphere overview)
+set it unconditionally, so the non-compact path no longer rendered
+anywhere. A new embedded consumer that needs host
+stats beside its workloads must render its own hosts table rather than
+re-growing metric cells in the shared group row.
WorkloadsSurface stays monitoring-first. It must not render a persistent
aggregate banner just because running VMs or system containers lack an
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index cd2ec5bf9e..fb2158b9ac 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -970,6 +970,11 @@ the page supplies it through the Workloads `tableTitle` slot and the shared
states. This heading alignment changes neither workload backup scope nor the
storage/recovery evidence carried by the adjacent Backup column and Backups
tab.
+The guest table's grouped node rows are identity-only dividers. Node stats and
+the node drawer stay with the Proxmox nodes table above it, so `ProxmoxPageSurface`
+passes `groupNodeDrawerMode: 'disabled'` and no host-metric option to the
+embedded Workloads state. Those dividers carry no backup or recovery evidence;
+the per-guest Backup column remains the overview's protection signal.
The overview's node-row action selects the shared workload node scope and
reveals the guest section. The title names the selected node and reports the
filtered guest count, including zero. Activating the selected node again or
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index f3c5938c4c..d505a37b64 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -2973,7 +2973,10 @@ canonical WebSocket snapshot, including the public `vmware-vsphere` alias for
the raw `vmware` source, rather than degrading a source-scoped page to periodic
REST refreshes. VMware Overview passes that same source-scoped snapshot into
the embedded Workloads state, so hosts and VMs share one inventory generation
-and one explicit refresh path.
+and one explicit refresh path. Beyond disabling the grouped host drawer, it
+passes no option that adds host metrics to grouped rows: the shared Workloads
+group row carries host identity only, and per-host stats stay in the page's
+own hosts table.
Its page-owned workload toolbar consumes the complete shared
`getWorkloadsMetricFilterProps` binding, so vSphere VMs expose the same Bars,
Trends, Details, History, range, and first-use discovery contract as every
diff --git a/frontend-modern/browser-verification/8ada2c9fe2438ea7.json b/frontend-modern/browser-verification/8ada2c9fe2438ea7.json
new file mode 100644
index 0000000000..4f8e022c8f
--- /dev/null
+++ b/frontend-modern/browser-verification/8ada2c9fe2438ea7.json
@@ -0,0 +1,49 @@
+{
+ "version": 1,
+ "base_sha": "c3adf0c7b9fa7f4283f7fd832a806659b009b51b",
+ "verified_at": "2026-10-06T21:45:33Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Workloads/WorkloadPanel.tsx",
+ "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
+ "frontend-modern/src/components/Workloads/WorkloadsTable.tsx",
+ "frontend-modern/src/components/Workloads/useWorkloadsState.ts",
+ "frontend-modern/src/components/shared/NodeGroupHeader.tsx",
+ "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx",
+ "frontend-modern/src/features/vmware/VmwarePageSurface.tsx"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "67c2c51ffceb96893f968dc141dbce54f0affe66fb389ffeeba8c8c94988968c",
+ "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "e49032445f566b4e6a4329314f4ada41ed6f7652ba52e9e3fc413047a49f7878",
+ "frontend-modern/src/components/Workloads/WorkloadsTable.tsx": "a340532f9e3e0e28d83e9a0fb369c8be973acb0dea7b321fd646345ff924db38",
+ "frontend-modern/src/components/Workloads/useWorkloadsState.ts": "a29034c5149b7323e86bc2fe795d9b18db805b1ad32e32013fb71547256d61f5",
+ "frontend-modern/src/components/shared/NodeGroupHeader.tsx": "e435c1e6ab04b85a022459cf19a67645bc8339f15ef986ffcb5b42ba27ce667f",
+ "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "80c860be3bee37251c16d21044074267e48991b9cb39b87d335cb2155066ec96",
+ "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "6c74063a298a259ce5f60485174529e9bffbe7293ae0710f53dc95d6f3c11265"
+ },
+ "routes": ["/proxmox/overview", "/vmware"],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Production vite build of this tree (rebased on c3adf0c7b9) embedded in a private DEMO_MODE mock backend (50 PVE nodes, 929 guests, 7 ESXi hosts), headless Chromium via Playwright; desktop 1440x900 and phone 390x844.",
+ "Proxmox Overview guest table, Grouped layout: every grouped node row (8 desktop, 2 phone in the rendered window) is one td whose colspan equals the guest table's visible header count (8 desktop, 5 phone), with status dot, linked node name, cluster badge and Agent badge only; no data-workload-col cells, no PVE version, temperature or uptime text.",
+ "vSphere Overview VM table: the 7 host group rows render as one spanning cell (colspan 6 desktop, 5 phone) with host name and cluster badge, unchanged shape.",
+ "Bars and Trends metric modes both inspected: Trends adds four sparklines per guest row (Proxmox 140 to 560 images, vSphere 18 to 72) while each grouped host row keeps exactly its one status-dot image.",
+ "Screenshots of grouped rows in both modes and of open guest drawers inspected at both viewports; no page errors.",
+ "This is the shape both pages already rendered on main, where compactGroupHeaders was always set; it is not a check of host drawers, which both pages disable."
+ ],
+ "interactions": [
+ "Opened the View popover (desktop) and the Filters popover (phone) and toggled Bars to Trends and back.",
+ "Clicked a grouped host row on each page: no inline node drawer row appears and nothing else changes.",
+ "Clicked the first guest row under a group on each page: its inline guest drawer opens.",
+ "Scrolled the first grouped host row to the viewport centre before each capture."
+ ]
+}
diff --git a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
index 9caf56f762..de8a3cab1d 100644
--- a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
@@ -1,10 +1,6 @@
import { createMemo, For, Show } from 'solid-js';
import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
-import { EnhancedCPUBar } from '@/components/Workloads/EnhancedCPUBar';
-import { MetricMiniSparkline } from '@/components/Workloads/MetricMiniSparkline';
-import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
-import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import {
GROUPED_TABLE_ROW_BADGE_CLASS,
getGroupedTableRowCellClass,
@@ -16,12 +12,9 @@ import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
import { createSummaryInteractiveRowPreviewHandlers } from '@/components/shared/summaryInteractionA11y';
import { buildSummaryDisclosureControlsId } from '@/components/shared/summaryInteractionA11y';
import { TableBody, TableCell, TableRow } from '@/components/shared/Table';
-import type { Node } from '@/types/api';
import { getAlertsForResource, getAlertStyles } from '@/utils/alerts';
import { guestOverrideIdCandidates } from '@/features/alerts/guestOverrideIdentity';
-import { formatSpeed, formatUptime } from '@/utils/format';
import { isNodeOnline } from '@/utils/status';
-import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature';
import { getCanonicalWorkloadId, getWorkloadMetadataIdCandidates } from '@/utils/workloads';
import {
resolveSummaryGroupMemberInteractionState,
@@ -34,14 +27,12 @@ import { NodeDrawer } from './NodeDrawer';
import { buildWorkloadSummaryGroupScope } from './workloadSelectors';
import { getWorkloadGuestMetadataRecord } from './workloadGuestMetadataRecord';
import type { WorkloadsState } from './useWorkloadsState';
-import type { WorkloadTableMetric } from './workloadMetricHistoryModel';
type WorkloadPanelProps = Pick<
WorkloadsState,
| 'activeAlerts'
| 'alertsEnabled'
| 'bottomSpacerHeight'
- | 'compactGroupHeaders'
| 'getGroupLabel'
| 'getNodeTemperatureThresholds'
| 'groupedGuests'
@@ -58,7 +49,6 @@ type WorkloadPanelProps = Pick<
| 'focusedSummaryWorkloadGroupScope'
| 'focusedSummaryWorkloadGroupId'
| 'hoveredSummaryWorkloadGroupScope'
- | 'isMobile'
| 'nestedWorkloadContextByGuestId'
| 'nodeByInstance'
| 'search'
@@ -80,289 +70,9 @@ type WorkloadPanelProps = Pick<
| 'workloadTableLayoutMode'
| 'workloadTableVisibleColumnIds'
| 'workloadColumnWidths'
- | 'workloadTableVisibleColumns'
>;
-const GROUP_NODE_METRIC_CELL_CLASS = 'px-1.5 sm:px-2 py-0.5 align-middle';
-const GROUP_NODE_NAME_CELL_CLASS = getGroupedTableRowCellClass(
- 'py-1! pl-2! sm:pl-3! pr-1.5! sm:pr-2!',
-);
-
-const getGroupNodeColumnCellClass = (_columnId: string, isNameColumn: boolean): string => {
- if (isNameColumn) return GROUP_NODE_NAME_CELL_CLASS;
- return GROUP_NODE_METRIC_CELL_CLASS;
-};
-
-const normalizeMetricPercent = (value: number | null | undefined): number => {
- if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
- return value <= 1 ? Math.max(0, value * 100) : Math.max(0, value);
-};
-
-const formatMetricPercent = (value: number | null | undefined): string =>
- `${Math.round(normalizeMetricPercent(value))}%`;
-
-const getUsedPercent = (used?: number | null, total?: number | null): number => {
- if (typeof used !== 'number' || typeof total !== 'number' || total <= 0) return 0;
- return normalizeMetricPercent((used / total) * 100);
-};
-
-const getGroupNodeMetricKey = (node: Node): string => node.linkedAgentId || node.id || node.name;
-
-const getGroupNodePveVersion = (node: Node): string => {
- const version = (node.pveVersion || '').trim();
- if (!version || version.toLowerCase() === 'unknown') return '';
- return (
- version.match(/pve-manager\/([^/\s]+)/i)?.[1] || version.match(/\d+(?:\.\d+)+/)?.[0] || version
- );
-};
-
-const renderGroupNodeEmptyCell = () => (
-
-
- —
-
-
-);
-
export function WorkloadPanel(props: WorkloadPanelProps) {
- const renderGroupNodeSparkline = (
- node: Node,
- metric: WorkloadTableMetric,
- valueLabel: string,
- title: string,
- unit = '%',
- valueLabelMode: 'inline' | 'tooltip' | 'hidden' = 'inline',
- formatValue?: (value: number) => string,
- ) => (
-
- );
-
- const renderGroupNodeRatePair = (
- leftLabel: string,
- leftClass: string,
- leftValue: number | null | undefined,
- rightLabel: string,
- rightClass: string,
- rightValue: number | null | undefined,
- ) => {
- const left = Math.max(0, leftValue ?? 0);
- const right = Math.max(0, rightValue ?? 0);
- if (left <= 0 && right <= 0) return renderGroupNodeEmptyCell();
-
- return (
-
- {leftLabel}
-
- {formatSpeed(left)}
-
- {rightLabel}
-
- {formatSpeed(right)}
-
-
- );
- };
-
- const renderGroupNodeInfoCell = (node: Node) => {
- const version = getGroupNodePveVersion(node);
- const temperature = getCpuTemperature(node.temperature);
- if (!version && temperature === null) return renderGroupNodeEmptyCell();
-
- return (
-
-
- PVE {version}
-
-
-
- {formatTemperature(temperature)}
-
-
-
- );
- };
-
- const renderGroupNodeColumnCell = (columnId: string, node: Node) => {
- switch (columnId) {
- case 'type':
- return (
-
-
- PVE
-
-
- );
- case 'info':
- case 'vmid':
- return renderGroupNodeInfoCell(node);
- case 'cpu':
- if (props.workloadMetricDisplayMode() === 'sparklines') {
- return renderGroupNodeSparkline(
- node,
- 'cpu',
- formatMetricPercent(node.cpu),
- `${node.name} CPU history`,
- '%',
- 'inline',
- formatMetricPercent,
- );
- }
- return (
-
-
-
- );
- case 'memory':
- if (props.workloadMetricDisplayMode() === 'sparklines') {
- return renderGroupNodeSparkline(
- node,
- 'memory',
- node.memory?.usageUnavailable
- ? 'N/A'
- : formatMetricPercent(getUsedPercent(node.memory?.used, node.memory?.total)),
- `${node.name} memory history`,
- '%',
- 'inline',
- formatMetricPercent,
- );
- }
- return (
-
-
-
- );
- case 'disk':
- if (props.workloadMetricDisplayMode() === 'sparklines') {
- return renderGroupNodeSparkline(
- node,
- 'disk',
- formatMetricPercent(node.disk?.usage),
- `${node.name} disk usage history`,
- '%',
- 'inline',
- formatMetricPercent,
- );
- }
- return (
-
-
-
- );
- case 'uptime':
- return (
-
- 0} fallback={renderGroupNodeEmptyCell()}>
- 0 && node.uptime < 3600 ? 'text-orange-500' : 'text-muted'
- }`}
- >
- {formatUptime(node.uptime, props.isMobile())}
-
-
-
- );
- case 'node':
- case 'context':
- return (
-
-
- {node.name}
-
-
- );
- case 'os':
- return (
-
- PVE
-
- );
- case 'netIo':
- if (props.workloadMetricDisplayMode() === 'sparklines') {
- return renderGroupNodeSparkline(
- node,
- 'netIo',
- `${formatSpeed(node.networkIn ?? 0)} / ${formatSpeed(node.networkOut ?? 0)}`,
- `${node.name} network I/O history`,
- 'B/s',
- 'tooltip',
- formatSpeed,
- );
- }
- return renderGroupNodeRatePair(
- '↓',
- 'text-emerald-500',
- node.networkIn,
- '↑',
- 'text-orange-400',
- node.networkOut,
- );
- case 'diskIo':
- if (props.workloadMetricDisplayMode() === 'sparklines') {
- return renderGroupNodeSparkline(
- node,
- 'diskIo',
- `${formatSpeed(node.diskRead ?? 0)} / ${formatSpeed(node.diskWrite ?? 0)}`,
- `${node.name} disk I/O history`,
- 'B/s',
- 'tooltip',
- formatSpeed,
- );
- }
- return renderGroupNodeRatePair(
- 'R',
- 'font-mono text-blue-500',
- node.diskRead,
- 'W',
- 'font-mono text-amber-500',
- node.diskWrite,
- );
- default:
- return renderGroupNodeEmptyCell();
- }
- };
-
return (
0}>
@@ -485,22 +195,8 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
>
td>div]:flex-nowrap'
diff --git a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
index 0f413620b6..fe11021d39 100644
--- a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
@@ -192,7 +192,6 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) {
activeAlerts={state.activeAlerts}
alertsEnabled={state.alertsEnabled}
bottomSpacerHeight={state.bottomSpacerHeight}
- compactGroupHeaders={state.compactGroupHeaders}
getGroupLabel={state.getGroupLabel}
getNodeTemperatureThresholds={state.getNodeTemperatureThresholds}
groupedGuests={state.groupedGuests}
diff --git a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
index 2f25d5598b..5193c87d1c 100644
--- a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
@@ -17,7 +17,6 @@ type WorkloadsTableProps = Pick<
| 'activeAlerts'
| 'alertsEnabled'
| 'bottomSpacerHeight'
- | 'compactGroupHeaders'
| 'getGroupLabel'
| 'getNodeTemperatureThresholds'
| 'groupedGuests'
@@ -133,7 +132,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
activeAlerts={props.activeAlerts}
alertsEnabled={props.alertsEnabled}
bottomSpacerHeight={props.bottomSpacerHeight}
- compactGroupHeaders={props.compactGroupHeaders}
getGroupLabel={props.getGroupLabel}
getNodeTemperatureThresholds={props.getNodeTemperatureThresholds}
groupedGuests={props.groupedGuests}
@@ -150,7 +148,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
focusedSummaryWorkloadGroupScope={props.focusedSummaryWorkloadGroupScope}
focusedSummaryWorkloadGroupId={props.focusedSummaryWorkloadGroupId}
hoveredSummaryWorkloadGroupScope={props.hoveredSummaryWorkloadGroupScope}
- isMobile={props.isMobile}
nestedWorkloadContextByGuestId={props.nestedWorkloadContextByGuestId}
nodeByInstance={props.nodeByInstance}
search={props.search}
@@ -171,7 +168,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
workloadMetricHistory={props.workloadMetricHistory}
workloadTableLayoutMode={props.workloadTableLayoutMode}
workloadTableVisibleColumnIds={props.workloadTableVisibleColumnIds}
- workloadTableVisibleColumns={props.workloadTableVisibleColumns}
workloadColumnWidths={props.workloadColumnWidths}
/>
diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx
index d2aa75fff9..7a272f8b5c 100644
--- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx
+++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx
@@ -454,32 +454,6 @@ describe('Workloads platform-page embed contract', () => {
expect(nodesTableSource).toContain('isSparklineMode');
});
- it('exposes compactGroupHeaders so platform pages can strip duplicate host stats from group rows', async () => {
- const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
- expect(stateSource).toContain('compactGroupHeaders?: boolean;');
- expect(stateSource).toContain('compactGroupHeaders: () => props.compactGroupHeaders === true,');
-
- const tableSource = (await import('../WorkloadsTable.tsx?raw')).default;
- expect(tableSource).toContain(`| 'compactGroupHeaders'`);
- expect(tableSource).toContain('compactGroupHeaders={props.compactGroupHeaders}');
-
- const panelSource = (await import('../WorkloadPanel.tsx?raw')).default;
- expect(panelSource).toContain(`| 'compactGroupHeaders'`);
- // When the flag is set the panel falls back to NodeGroupHeader's
- // colspan layout: no per-column metric cells, no inline node facts.
- expect(panelSource).toContain(
- 'props.compactGroupHeaders() ? undefined : props.workloadTableVisibleColumns()',
- );
- expect(panelSource).toContain(
- 'props.compactGroupHeaders() ? undefined : renderGroupNodeColumnCell',
- );
- expect(panelSource).toContain('!props.compactGroupHeaders() &&');
-
- const proxmoxSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw'))
- .default;
- expect(proxmoxSource).toContain('compactGroupHeaders');
- });
-
it('lets platform pages move grouped host drawers to a dedicated host table owner', async () => {
const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
expect(stateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';");
diff --git a/frontend-modern/src/components/Workloads/useWorkloadsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsState.ts
index e041d9d20c..195099789b 100644
--- a/frontend-modern/src/components/Workloads/useWorkloadsState.ts
+++ b/frontend-modern/src/components/Workloads/useWorkloadsState.ts
@@ -105,12 +105,6 @@ export interface WorkloadsSurfaceProps {
}>;
resourceSnapshotRefetch?: () => Promise;
statusModeStorageScope?: string;
- // Platform pages that render their own hosts table above the embedded
- // workloads surface (e.g. Proxmox overview) own the per-host CPU / Memory
- // / Disk / Temperature / uptime / version stats. Setting
- // `compactGroupHeaders` strips those stats from the NodeGroupHeader rows
- // in grouped mode so the section dividers don't duplicate the info.
- compactGroupHeaders?: boolean;
// Default Workloads behavior owns grouped host row drawers inline. Platform
// pages with a dedicated host table can disable that drawer so host details
// open from the host-owned table instead of the embedded guest table.
@@ -728,7 +722,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
workloadsNoInventoryState,
ws,
groupingMode,
- compactGroupHeaders: () => props.compactGroupHeaders === true,
groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',
groupLabelBadges,
} as const;
diff --git a/frontend-modern/src/components/shared/NodeGroupHeader.tsx b/frontend-modern/src/components/shared/NodeGroupHeader.tsx
index 0c3b9feb69..2dfa8db0fc 100644
--- a/frontend-modern/src/components/shared/NodeGroupHeader.tsx
+++ b/frontend-modern/src/components/shared/NodeGroupHeader.tsx
@@ -1,12 +1,8 @@
-import { Component, For, Show, type JSX } from 'solid-js';
+import { Component, Show, type JSX } from 'solid-js';
import type { Node } from '@/types/api';
-import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
import { getNodeDisplayName, getNodeExternalUrl, hasAlternateDisplayName } from '@/utils/nodes';
import { StatusDot } from '@/components/shared/StatusDot';
import { getNodeStatusIndicator } from '@/utils/status';
-import { formatUptime } from '@/utils/format';
-import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature';
-import { formatProxmoxVersion } from '@/utils/proxmoxVersion';
import {
GROUPED_TABLE_ROW_BADGE_CLASS,
getGroupedTableRowCellClass,
@@ -17,13 +13,8 @@ import { ResourceNameWithWebInterfaceLink } from './WebInterfaceLink';
interface NodeGroupHeaderProps {
node: Node;
colspan?: number;
- columns?: Array<{ id: string }>;
- columnCellClass?: (columnId: string, isNameColumn: boolean) => string;
leadingAction?: JSX.Element;
- renderColumnCell?: (columnId: string, node: Node) => JSX.Element;
renderAs?: 'tr' | 'div';
- showFactsInName?: boolean;
- temperatureThresholds?: MetricDisplayThresholds | null;
trClass?: string;
trProps?: JSX.HTMLAttributes &
Partial>;
@@ -35,12 +26,6 @@ export const NodeGroupHeader: Component = (props) => {
const nodeUrl = () => getNodeExternalUrl(props.node);
const displayName = () => getNodeDisplayName(props.node);
const showActualName = () => hasAlternateDisplayName(props.node);
- const pveVersion = () => formatProxmoxVersion(props.node.pveVersion);
- const cpuTemperature = () => getCpuTemperature(props.node.temperature);
- const hasNodeFacts = () =>
- Boolean(pveVersion()) ||
- cpuTemperature() !== null ||
- (typeof props.node.uptime === 'number' && props.node.uptime > 0);
const InnerContent = () => (
= (props) => {
Agent
-
-
-
-
- PVE {pveVersion()}
-
-
-
- {formatTemperature(cpuTemperature())}
-
-
- 0}>
- {formatUptime(props.node.uptime, true)}
-
-
-
);
@@ -118,38 +84,9 @@ export const NodeGroupHeader: Component = (props) => {
}
>
- 0 && Boolean(props.renderColumnCell)}
- fallback={
- |
-
- |
- }
- >
-
- {(column) => {
- const isNameColumn = () => column.id === 'name';
- return (
-
-
-
-
- |
- );
- }}
-
-
+
+
+ |
);
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
index 679ab96c40..7697db0332 100644
--- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
+++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
@@ -216,6 +216,7 @@ import proxmoxCoverageTableSource from '@/features/proxmox/ProxmoxCoverageTable.
import proxmoxMailGatewayTableSource from '@/features/proxmox/ProxmoxMailGatewayTable.tsx?raw';
import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw';
import proxmoxHostTableModelSource from '@/features/proxmox/proxmoxHostTableModel.ts?raw';
+import proxmoxPageModelSource from '@/features/proxmox/proxmoxPageModel.ts?raw';
import proxmoxRecoverableTableSource from '@/features/proxmox/ProxmoxRecoverableTable.tsx?raw';
import proxmoxReplicationTableSource from '@/features/proxmox/ProxmoxReplicationTable.tsx?raw';
import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw';
@@ -1406,9 +1407,9 @@ describe('shared primitive guardrails', () => {
});
it('routes Proxmox node version presentation through the shared formatter', () => {
- expect(nodeGroupHeaderSource).toContain("from '@/utils/proxmoxVersion'");
- expect(nodeGroupHeaderSource).toContain('formatProxmoxVersion(props.node.pveVersion)');
- expect(nodeGroupHeaderSource).not.toContain('pve-manager\\/');
+ expect(proxmoxPageModelSource).toContain("from '@/utils/proxmoxVersion'");
+ expect(proxmoxPageModelSource).toContain('formatProxmoxVersion(resource.proxmox?.pveVersion)');
+ expect(proxmoxPageModelSource).not.toContain('pve-manager\\/');
expect(proxmoxVersionSource).toContain('formatProxmoxVersion');
expect(proxmoxVersionSource).toContain('pve-manager\\/');
diff --git a/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx b/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
index 88f23a0606..f31f9a4ea3 100644
--- a/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
@@ -1,9 +1,8 @@
import { render, screen } from '@solidjs/testing-library';
-import { beforeEach, describe, expect, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
import type { Node } from '@/types/api';
-import { temperatureStore } from '@/utils/temperature';
const makeNode = (overrides: Partial = {}): Node => ({
id: 'cluster-a-pve1',
@@ -34,18 +33,11 @@ const makeNode = (overrides: Partial = {}): Node => ({
});
describe('NodeGroupHeader', () => {
- beforeEach(() => {
- temperatureStore.setUnit('celsius');
- });
-
- it('renders compact infrastructure facts in the grouped workload header', () => {
+ it('renders node identity in the grouped workload header', () => {
render(() => );
expect(screen.getByText('pve1')).toBeInTheDocument();
expect(screen.getByText('homelab')).toBeInTheDocument();
- expect(screen.getByText('PVE 9.1.9')).toBeInTheDocument();
- expect(screen.getByText('62°C')).toBeInTheDocument();
- expect(screen.getByText('2h')).toBeInTheDocument();
const link = screen.getByRole('link', { name: 'Open web interface for pve1' });
expect(link).toHaveAttribute('href', 'https://pve1:8006');
@@ -53,20 +45,21 @@ describe('NodeGroupHeader', () => {
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
- it('colors grouped header temperatures from configured thresholds', () => {
- render(() => (
-
+ it('spans the table in one cell and leaves host stats to the platform host table', () => {
+ const { container } = render(() => (
+
));
- expect(screen.getByText('76°C')).toHaveClass('text-green-600');
+ const cells = container.querySelectorAll('tr > td');
+ expect(cells).toHaveLength(1);
+ expect(cells[0]).toHaveAttribute('colspan', '9');
+ expect(cells[0]).toHaveTextContent('pve1');
+ expect(screen.queryByText('PVE 9.1.9')).not.toBeInTheDocument();
+ expect(screen.queryByText(/°[CF]/)).not.toBeInTheDocument();
+ expect(screen.queryByText('2h')).not.toBeInTheDocument();
});
});
diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
index 0cf4d4c4ba..e34418e333 100644
--- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
+++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
@@ -495,7 +495,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
// surface, so label it with the platform vocabulary instead of 'Info'.
columnLabelOverrides: { info: 'ID' },
statusModeStorageScope: PROXMOX_WORKLOAD_STATUS_STORAGE_SCOPE,
- compactGroupHeaders: true,
groupNodeDrawerMode: 'disabled',
metricDisplayMode: props.metricDisplayMode,
onMetricDisplayModeChange: props.setMetricDisplayMode,
@@ -638,7 +637,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
forcedPlatform={PROXMOX_PLATFORM_FILTER}
excludedWorkloadTypes={PROXMOX_WORKLOAD_EXCLUDED_TYPES}
showNestedExcludedWorkloads
- compactGroupHeaders
groupNodeDrawerMode="disabled"
suppressFilterToolbar
emptyStateTitle="No Proxmox workloads"
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 0979b3a487..198c6b273c 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
@@ -214,6 +214,16 @@ describe('ProxmoxPageSurface contract', () => {
);
});
+ it('leaves host details to the nodes table instead of the embedded guest table', () => {
+ setResources([makeResource({ id: 'vm-1', type: 'vm' })]);
+
+ renderSurface();
+
+ expect(mockWorkloadsOptions).toHaveBeenCalledWith(
+ expect.objectContaining({ groupNodeDrawerMode: 'disabled' }),
+ );
+ });
+
afterEach(() => {
cleanup();
vi.clearAllMocks();
diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
index 0e8d015897..1a34ea8be5 100644
--- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
+++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
@@ -474,7 +474,6 @@ function VmwareOverview(props: VmwareOverviewProps) {
statusModeStorageScope: VMWARE_WORKLOAD_STATUS_STORAGE_SCOPE,
columnVisibilityStorageScope: VMWARE_WORKLOAD_COLUMN_VISIBILITY_SCOPE,
additionalDefaultHiddenColumnIds: [...VMWARE_WORKLOAD_DEFAULT_HIDDEN_COLUMN_IDS],
- compactGroupHeaders: true,
inventorySourcesQuery: props.inventorySourcesQuery,
groupNodeDrawerMode: 'disabled',
metricDisplayMode: props.metricDisplayMode,
@@ -545,7 +544,6 @@ function VmwareOverview(props: VmwareOverviewProps) {
useWorkloads
forcedPlatform={VMWARE_PLATFORM_FILTER}
forcedViewMode="vm"
- compactGroupHeaders
groupNodeDrawerMode="disabled"
suppressFilterToolbar
emptyStateTitle="No vSphere VMs"
diff --git a/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts
index 3f8e8898e8..37c1175902 100644
--- a/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts
+++ b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts
@@ -2,15 +2,15 @@ import { describe, expect, it } from 'vitest';
import nodeDrawerOverviewSource from '@/components/Workloads/NodeDrawerOverview.tsx?raw';
import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
-import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw';
import temperatureGaugeSource from '@/components/shared/TemperatureGauge.tsx?raw';
import dockerHostDrawerOverviewSource from '@/features/docker/DockerHostDrawerOverview.tsx?raw';
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
describe('temperature display guardrails', () => {
it('keeps colored temperature displays threshold-aware', () => {
- expect(nodeGroupHeaderSource).toContain('temperatureThresholds');
- expect(workloadPanelSource).toContain('getNodeTemperatureThresholds(node)');
+ expect(workloadPanelSource).toContain(
+ 'temperatureThresholds={props.getNodeTemperatureThresholds(node()!)}',
+ );
expect(nodeDrawerOverviewSource).toContain('getTemperatureTextClass(value, thresholds)');
expect(dockerHostDrawerOverviewSource).toContain('temperatureThresholds()');
expect(agentsMachinesTableSource).toContain('getAgentMachineTemperatureMetric');
@@ -19,7 +19,6 @@ describe('temperature display guardrails', () => {
});
it('does not reintroduce the previous local temperature severity calls', () => {
- expect(nodeGroupHeaderSource).not.toContain('getTemperatureTextClass(cpuTemperature())');
expect(workloadPanelSource).not.toContain('getTemperatureTextClass(temperature)}');
expect(nodeDrawerOverviewSource).not.toContain('getTemperatureTextClass(value)');
expect(dockerHostDrawerOverviewSource).not.toContain('getTemperatureTextClass(temperature)');