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