fix: apply empty agent updates immediately when existing agents are stale

The anti-flapping logic previously required 3 consecutive empty updates
before clearing stale agents from the UI. Now if all existing hosts/dockerHosts
have lastSeen > 60 seconds ago, empty updates are applied immediately.

This fixes the issue where removed agents stayed visible for too long.
This commit is contained in:
rcourtman 2025-12-13 22:41:15 +00:00
parent b02a1556b7
commit 68456763f9

View file

@ -334,17 +334,17 @@ export function createWebSocketStore(url: string) {
try { try {
const message: WSMessage = data; const message: WSMessage = data;
if ( if (
message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE || message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE ||
message.type === WEBSOCKET.MESSAGE_TYPES.RAW_DATA message.type === WEBSOCKET.MESSAGE_TYPES.RAW_DATA
) { ) {
// Update state properties individually, but batch the whole payload to // Update state properties individually, but batch the whole payload to
// reduce reactive recomputations and UI thrash on large updates. // reduce reactive recomputations and UI thrash on large updates.
if (message.data) batch(() => { if (message.data) batch(() => {
// Mark that we've received usable data (initial payload or raw update) // Mark that we've received usable data (initial payload or raw update)
if (!initialDataReceived()) { if (!initialDataReceived()) {
setInitialDataReceived(true); setInitialDataReceived(true);
} }
// Only update if we have actual data, don't overwrite with empty arrays // Only update if we have actual data, don't overwrite with empty arrays
if (message.data.nodes !== undefined) { if (message.data.nodes !== undefined) {
@ -443,14 +443,25 @@ export function createWebSocketStore(url: string) {
if (incomingHosts.length === 0) { if (incomingHosts.length === 0) {
consecutiveEmptyDockerUpdates += 1; consecutiveEmptyDockerUpdates += 1;
// Check if all existing docker hosts are stale (>60s since lastSeen)
// If so, they're probably really gone - apply the empty update immediately
const now = Date.now();
const staleThresholdMs = 60_000; // 60 seconds
const existingDockerHosts = state.dockerHosts || [];
const allStale = existingDockerHosts.length === 0 || existingDockerHosts.every(
(h) => !h.lastSeen || (now - h.lastSeen) > staleThresholdMs
);
shouldApplyDockerHosts = shouldApplyDockerHosts =
!hasReceivedNonEmptyDockerHosts || !hasReceivedNonEmptyDockerHosts ||
allStale ||
consecutiveEmptyDockerUpdates >= 3 || consecutiveEmptyDockerUpdates >= 3 ||
message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE; message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE;
if (shouldApplyDockerHosts) { if (shouldApplyDockerHosts) {
logger.debug('[WebSocket] Updating dockerHosts', { logger.debug('[WebSocket] Updating dockerHosts', {
count: incomingHosts.length, count: incomingHosts.length,
reason: allStale ? 'allStale' : 'threshold',
}); });
processedDockerHosts = mergeDockerHostRevocations(incomingHosts); processedDockerHosts = mergeDockerHostRevocations(incomingHosts);
} else { } else {
@ -486,14 +497,25 @@ export function createWebSocketStore(url: string) {
if (incomingHosts.length === 0) { if (incomingHosts.length === 0) {
consecutiveEmptyHostUpdates += 1; consecutiveEmptyHostUpdates += 1;
// Check if all existing hosts are stale (>60s since lastSeen)
// If so, they're probably really gone - apply the empty update immediately
const now = Date.now();
const staleThresholdMs = 60_000; // 60 seconds
const existingHosts = state.hosts || [];
const allHostsStale = existingHosts.length === 0 || existingHosts.every(
(h) => !h.lastSeen || (now - h.lastSeen) > staleThresholdMs
);
shouldApplyHosts = shouldApplyHosts =
!hasReceivedNonEmptyHosts || !hasReceivedNonEmptyHosts ||
allHostsStale ||
consecutiveEmptyHostUpdates >= 3 || consecutiveEmptyHostUpdates >= 3 ||
message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE; message.type === WEBSOCKET.MESSAGE_TYPES.INITIAL_STATE;
if (shouldApplyHosts) { if (shouldApplyHosts) {
logger.debug('[WebSocket] Updating hosts', { logger.debug('[WebSocket] Updating hosts', {
count: incomingHosts.length, count: incomingHosts.length,
reason: allHostsStale ? 'allStale' : 'threshold',
}); });
processedHosts = mergeHostRevocations(incomingHosts); processedHosts = mergeHostRevocations(incomingHosts);
} else { } else {
@ -646,11 +668,11 @@ export function createWebSocketStore(url: string) {
// Updated recentlyResolved // Updated recentlyResolved
} }
setState('lastUpdate', message.data.lastUpdate || new Date().toISOString()); setState('lastUpdate', message.data.lastUpdate || new Date().toISOString());
}); });
logger.debug('message', { logger.debug('message', {
type: message.type, type: message.type,
hasData: !!message.data, hasData: !!message.data,
nodeCount: message.data?.nodes?.length || 0, nodeCount: message.data?.nodes?.length || 0,
vmCount: message.data?.vms?.length || 0, vmCount: message.data?.vms?.length || 0,
containerCount: message.data?.containers?.length || 0, containerCount: message.data?.containers?.length || 0,