From be4469e5223af3965a18608e62ea28db7321d96e Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:49:40 +0100 Subject: [PATCH] Label vSphere host groups with their cluster, not a VM On the vSphere overview every ESXi host group header carried a chip naming the host's first VM (esxi-01.lab.local [etl-batch-01]) where the cluster belongs. The workload adapter took the cluster from getPreferredResourceClusterName, which falls back to the resource's own name. That fallback suits a cluster resource, but a vSphere VM keeps its cluster in vmware.clusterName, which the helper never reads, so the VM became its own cluster. Standalone Proxmox guests had the same problem in their context label. Workloads now take the cluster only from an explicit source: the vSphere facet, then the existing explicit helper. The grouping instance keeps its old fallback chain, so group keys do not move. Contract-Neutral: Behavioral bug fix: workload cluster labels stop falling back to the workload's own name; no contract delta. --- .../b63287638d6fa652.json | 35 +++++++++++++ .../src/hooks/__tests__/useWorkloads.test.ts | 52 +++++++++++++++++++ frontend-modern/src/hooks/useWorkloads.ts | 15 ++++-- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 frontend-modern/browser-verification/b63287638d6fa652.json diff --git a/frontend-modern/browser-verification/b63287638d6fa652.json b/frontend-modern/browser-verification/b63287638d6fa652.json new file mode 100644 index 0000000000..a99856d7ad --- /dev/null +++ b/frontend-modern/browser-verification/b63287638d6fa652.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "base_sha": "f5157115b7b224000ef7035285d9ce64bbe60bb7", + "verified_at": "2026-10-04T22:46:28Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/hooks/useWorkloads.ts" + ], + "content_sha256": { + "frontend-modern/src/hooks/useWorkloads.ts": "ef6459b28c2ce225e5f5ec56731f135b1a2b1788af40aad14724e108167d86ec" + }, + "routes": [ + "/vmware/overview", + "/proxmox/overview" + ], + "viewports": [ + { + "width": 1440, + "height": 1100 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Mock estate (PULSE_MOCK_MODE, 5 Proxmox nodes in one cluster, Lab vCenter with 7 ESXi hosts in Production Cluster and Edge Services). Before the fix every vSphere host group header carried a chip naming its first VM (esxi-01.lab.local [etl-batch-01]); after it each header names the host cluster and matches the hosts table Cluster column.", + "Proxmox overview group headers still render the node row with the Production West cluster chip and Agent badge, unchanged.", + "Phone width (390x844, touch, DPR 2): vSphere group headers show Production Cluster / Edge Services chips without truncation.", + "Unit proof: useWorkloads test fails on the old helper (Received etl-batch-01) and passes with the explicit cluster; a standalone Proxmox guest now has no cluster and labels its context with the instance." + ], + "interactions": [ + "Loaded each route signed in with Playwright Chromium at desktop and phone widths and inspected the screenshots; no console errors besides the dev-build update check." + ] +} diff --git a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts index e0c4a0eac0..fe512b6f09 100644 --- a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts +++ b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts @@ -1326,6 +1326,58 @@ describe('useWorkloads', () => { dispose(); }); + it('never labels a vSphere or standalone Proxmox workload as its own cluster', async () => { + apiFetchJSONMock.mockResolvedValueOnce({ + data: [ + { + ...sampleResource, + id: 'vmware-vm-etl', + name: 'etl-batch-01', + sources: ['vmware'], + parentName: 'esxi-01.lab.local', + proxmox: undefined, + vmware: { + managedObjectId: 'vm-206', + runtimeHostName: 'esxi-01.lab.local', + clusterName: 'Production Cluster', + connectionName: 'Lab vCenter', + powerState: 'poweredOn', + }, + }, + { + ...sampleResource, + id: 'pve-home-pve1-102', + name: 'standalone-vm', + vmid: 102, + instance: 'pve-home', + proxmox: {}, + }, + ], + meta: { totalPages: 1 }, + }); + + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + const [enabled] = createSignal(true); + result = useWorkloads(enabled); + }); + + await flushAsync(); + await waitForWorkloadCount(() => result!.workloads().length, 2); + + const byName = new Map(result!.workloads().map((workload) => [workload.name, workload])); + expect(byName.get('etl-batch-01')?.clusterName).toBe('Production Cluster'); + expect(byName.get('etl-batch-01')?.contextLabel).toBe('esxi-01.lab.local (Production Cluster)'); + expect(byName.get('etl-batch-01')?.instance).toBe('Production Cluster'); + expect(byName.get('standalone-vm')?.clusterName).toBeUndefined(); + expect(byName.get('standalone-vm')?.contextLabel).toBe('pve1 (pve-home)'); + expect(byName.get('standalone-vm')?.instance).toBe('pve-home'); + + dispose(); + }); + it('uses the shared cluster-name helper for proxmox workload labels', async () => { apiFetchJSONMock.mockResolvedValueOnce({ data: [ diff --git a/frontend-modern/src/hooks/useWorkloads.ts b/frontend-modern/src/hooks/useWorkloads.ts index 605418bc57..26540a4829 100644 --- a/frontend-modern/src/hooks/useWorkloads.ts +++ b/frontend-modern/src/hooks/useWorkloads.ts @@ -20,6 +20,7 @@ import { getPreferredResourceClusterName, getPreferredResourceKubernetesContext, } from '@/utils/resourceIdentity'; +import { getExplicitResourceClusterName } from '@/utils/agentResources'; import type { WorkloadGuest } from '@/types/workloads'; import type { HostRAIDArray } from '@/types/api'; import type { @@ -448,6 +449,14 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => { ''; const kubernetesContext = getPreferredResourceKubernetesContext(resource); const preferredClusterName = getPreferredResourceClusterName(resource); + // The preferred helper falls back to the resource's own name, which is the + // right label for a cluster resource but not for a workload: a vSphere VM + // or a standalone Proxmox guest would otherwise be labelled as its own + // cluster. Only an explicit cluster may become the workload's cluster. + const workloadClusterName = + (platformType === 'vmware-vsphere' + ? resource.vmware?.clusterName?.trim() || undefined + : undefined) ?? getExplicitResourceClusterName(resource); const instance = vmwareInstance ?? resource.instance ?? @@ -638,7 +647,7 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => { workloadType === 'vm' || workloadType === 'system-container' ? node ? (() => { - const cluster = preferredClusterName || ''; + const cluster = workloadClusterName || ''; if (cluster && cluster !== node) return `${node} (${cluster})`; if (instance && instance !== node) return `${node} (${instance})`; return node; @@ -647,9 +656,9 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => { : workloadType === 'app-container' ? resource.parentName || resource.docker?.hostname : workloadType === 'pod' - ? preferredClusterName + ? workloadClusterName : undefined, - clusterName: preferredClusterName || undefined, + clusterName: workloadClusterName || undefined, containerRuntime: workloadType === 'app-container' ? (resource.docker?.runtime || '').trim() || undefined