From d7a0d97fa224913c69c6fe7250c4b8bf41b68324 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 21 Jul 2026 14:35:10 +0100 Subject: [PATCH] Subscribe workloads guest metadata to the canonical metadata event The workloads metadata state still listened for the legacy pulse:metadata-changed event, which no code dispatches since URL saves moved to dispatchResourceMetadataChanged, and its handler rewrote 3-part canonical guest ids into the v5-era instance-vmid shape that no lookup uses. In-page saves only stayed live through the drawer's direct callback; any other dispatcher was invisible. Listen for pulse:resource-metadata-changed and apply updates under the dispatched metadata id, ignoring agent-kind events that never key workload rows. Refs #1556 Contract-Neutral: behavioral fix: workloads metadata state listened for a legacy event nothing dispatches; no public contract change (#1556) --- ...loadsSurface.performance.contract.test.tsx | 2 +- .../useWorkloadGuestMetadataState.test.ts | 39 +++++++++++++++++++ .../useWorkloadGuestMetadataState.ts | 38 +++++++----------- 3 files changed, 54 insertions(+), 25 deletions(-) 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 9f07745b3..ea53c597c 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 @@ -1041,7 +1041,7 @@ describe('Workloads performance contract', () => { expect(workloadsGuestMetadataStateSource).toContain('GuestMetadataAPI.getAllMetadata()'); expect(workloadsGuestMetadataStateSource).toContain("eventBus.on('org_switched'"); expect(workloadsGuestMetadataStateSource).toContain( - "window.addEventListener('pulse:metadata-changed'", + 'window.addEventListener(RESOURCE_METADATA_CHANGED_EVENT', ); expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadUrlSync'); expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadFilterOptions'); diff --git a/frontend-modern/src/components/Workloads/__tests__/useWorkloadGuestMetadataState.test.ts b/frontend-modern/src/components/Workloads/__tests__/useWorkloadGuestMetadataState.test.ts index a5f62f04c..b4af1b7ff 100644 --- a/frontend-modern/src/components/Workloads/__tests__/useWorkloadGuestMetadataState.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/useWorkloadGuestMetadataState.test.ts @@ -1,7 +1,12 @@ import { createRoot } from 'solid-js'; +import { renderHook } from '@solidjs/testing-library'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWorkloadGuestMetadataState } from '../useWorkloadGuestMetadataState'; +import { + RESOURCE_METADATA_CHANGED_EVENT, + type ResourceMetadataChangedDetail, +} from '@/utils/resourceMetadataEvents'; vi.mock('@/api/guestMetadata', () => ({ GuestMetadataAPI: { @@ -50,4 +55,38 @@ describe('useWorkloadGuestMetadataState', () => { dispose(); }); }); + + it('applies resource-metadata-changed events under the dispatched metadata id', () => { + const { result, cleanup } = renderHook(() => useWorkloadGuestMetadataState()); + + window.dispatchEvent( + new CustomEvent(RESOURCE_METADATA_CHANGED_EVENT, { + detail: { + metadataKind: 'guest', + metadataId: 'pve1:node1:104', + customUrl: 'https://svc.example.lan', + }, + }), + ); + + expect(result.guestMetadata()['pve1:node1:104']?.customUrl).toBe('https://svc.example.lan'); + cleanup(); + }); + + it('ignores agent metadata events', () => { + const { result, cleanup } = renderHook(() => useWorkloadGuestMetadataState()); + + window.dispatchEvent( + new CustomEvent(RESOURCE_METADATA_CHANGED_EVENT, { + detail: { + metadataKind: 'agent', + metadataId: 'machine-1', + customUrl: 'https://machine.example.lan', + }, + }), + ); + + expect(result.guestMetadata()['machine-1']).toBeUndefined(); + cleanup(); + }); }); diff --git a/frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts b/frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts index 3c6b472db..2ae402763 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts @@ -5,6 +5,10 @@ import { getOrgID } from '@/utils/apiClient'; import { logger } from '@/utils/logger'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { normalizeOrgScope } from '@/utils/orgScope'; +import { + RESOURCE_METADATA_CHANGED_EVENT, + type ResourceMetadataChangedDetail, +} from '@/utils/resourceMetadataEvents'; import { eventBus } from '@/stores/events'; type GuestMetadataRecord = Record; @@ -296,43 +300,29 @@ export function useWorkloadGuestMetadataState() { void refreshGuestMetadata(); const handleMetadataChanged = (event: Event) => { - const customEvent = event as CustomEvent; - logger.debug('[Workloads] Metadata changed event received', customEvent.detail); + const detail = (event as CustomEvent).detail; + logger.debug('[Workloads] Metadata changed event received', detail); - if (customEvent.detail?.payload) { - let { guestId, url } = customEvent.detail.payload; - if (guestId) { - if (guestId.includes(':')) { - const parts = guestId.split(':'); - if (parts.length === 3) { - const [instance, _node, vmid] = parts; - guestId = `${instance}-${vmid}`; - logger.debug('[Workloads] Normalized optimistic guestId', { - original: customEvent.detail.payload.guestId, - normalized: guestId, - }); - } - } + // Agent (host machine) metadata never keys workload rows; only guest and + // docker app-container URLs surface in the workloads table. + if (detail?.metadataKind === 'agent') return; - logger.debug('[Workloads] Applying optimistic metadata update', { guestId, url }); - handleCustomUrlUpdate(guestId, url || ''); - return; - } + if (detail?.metadataId) { + handleCustomUrlUpdate(detail.metadataId, detail.customUrl ?? ''); + return; } - logger.debug('Metadata changed event received, refreshing...'); void refreshGuestMetadata(); }; - logger.debug('[Workloads] Adding pulse:metadata-changed listener'); - window.addEventListener('pulse:metadata-changed', handleMetadataChanged); + window.addEventListener(RESOURCE_METADATA_CHANGED_EVENT, handleMetadataChanged); const unsubscribeOrgSwitched = eventBus.on('org_switched', (nextOrgID) => { setOrgScope(normalizeOrgScope(nextOrgID)); void refreshGuestMetadata(); }); onCleanup(() => { - window.removeEventListener('pulse:metadata-changed', handleMetadataChanged); + window.removeEventListener(RESOURCE_METADATA_CHANGED_EVENT, handleMetadataChanged); unsubscribeOrgSwitched(); }); });