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)
This commit is contained in:
rcourtman
2026-07-21 14:35:10 +01:00
parent e339af2a6e
commit d7a0d97fa2
3 changed files with 54 additions and 25 deletions
@@ -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');
@@ -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<ResourceMetadataChangedDetail>(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<ResourceMetadataChangedDetail>(RESOURCE_METADATA_CHANGED_EVENT, {
detail: {
metadataKind: 'agent',
metadataId: 'machine-1',
customUrl: 'https://machine.example.lan',
},
}),
);
expect(result.guestMetadata()['machine-1']).toBeUndefined();
cleanup();
});
});
@@ -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<string, GuestMetadata>;
@@ -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<ResourceMetadataChangedDetail>).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();
});
});