diff --git a/neode-ui/src/views/fleet/__tests__/FleetFreshness.test.ts b/neode-ui/src/views/fleet/__tests__/FleetFreshness.test.ts index 7f46603c..af41e292 100644 --- a/neode-ui/src/views/fleet/__tests__/FleetFreshness.test.ts +++ b/neode-ui/src/views/fleet/__tests__/FleetFreshness.test.ts @@ -46,3 +46,25 @@ it('does not rearm timers when requests finish after unmount', async () => { expect(intervals).toHaveBeenCalledTimes(intervalCalls) intervals.mockRestore(); clear.mockRestore() }) + +it('keeps a late node history response out of the newly selected node', async () => { + const history = new Map void>() + vi.mocked(rpcClient.call).mockImplementation(({ method, params }) => { + if (method === 'telemetry.fleet-node-history') return new Promise(done => { history.set((params as { node_id: string }).node_id, done) }) as never + return Promise.resolve({ nodes: [], alerts: [] }) as never + }) + let fleet!: ReturnType + const wrapper = mount(defineComponent({ setup() { fleet = useFleetData(); return () => null } })) + await flushPromises() + fleet.selectNode('first'); await flushPromises() + fleet.selectNode('second'); await flushPromises() + history.get('first')!({ history: [{ timestamp: 'old', cpu_pct: 99 }] }) + await flushPromises() + expect(fleet.nodeHistory.value).toEqual([]) + expect(fleet.nodeHistoryLoading.value).toBe(true) + history.get('second')!({ history: [{ timestamp: 'current', cpu_pct: 10 }] }) + await flushPromises() + expect(fleet.nodeHistory.value[0]?.timestamp).toBe('current') + expect(fleet.nodeHistoryLoading.value).toBe(false) + wrapper.unmount() +}) diff --git a/neode-ui/src/views/fleet/useFleetData.ts b/neode-ui/src/views/fleet/useFleetData.ts index eef790c2..16d2bee3 100644 --- a/neode-ui/src/views/fleet/useFleetData.ts +++ b/neode-ui/src/views/fleet/useFleetData.ts @@ -273,6 +273,7 @@ export function useFleetData() { const sortBy = ref(cached.sortBy ?? 'status') const chartWidth = ref(300) let pollTimer: ReturnType | null = null + let historyRequest = 0 // --- Computed --- @@ -384,17 +385,20 @@ export function useFleetData() { } async function fetchNodeHistory(nodeId: string) { + const request = ++historyRequest nodeHistoryLoading.value = true try { const data = await rpcClient.call<{ history?: NodeHistoryEntry[]; entries?: NodeHistoryEntry[] }>({ method: 'telemetry.fleet-node-history', params: { node_id: nodeId }, }) - nodeHistory.value = normalizeNodeHistoryResponse(data) + if (request === historyRequest && selectedNodeId.value === nodeId) { + nodeHistory.value = normalizeNodeHistoryResponse(data) + } } catch { // Non-critical } finally { - nodeHistoryLoading.value = false + if (request === historyRequest) nodeHistoryLoading.value = false } } @@ -477,6 +481,9 @@ export function useFleetData() { // Fetch node history when selection changes watch(selectedNodeId, (newId) => { + historyRequest++ + nodeHistory.value = [] + nodeHistoryLoading.value = false if (newId) { fetchNodeHistory(newId) } else {