[grade=B] fix: stop aggressive API polling and SSE reconnect when auth lost
When unauthenticated (TOTP gate active), the dashboard was polling /api/v1/services/status every few seconds and reconnecting SSE every 2-8s indefinitely. Now: 401/403 sets _dcAuthLost flag to skip the polling interval entirely (no fallback to direct probes which misleadingly mark services as healthy), SSE exposes _sseClose() coordinated with _dcAuthLost, and queued refreshes are guarded.
This commit is contained in:
Vendored
+77
-77
File diff suppressed because one or more lines are too long
+14
-3
@@ -349,6 +349,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
|
// Skip if auth has been lost (e.g. TOTP gate activated externally).
|
||||||
|
// The polling interval in init.js also checks this flag.
|
||||||
|
if (window._dcAuthLost) return;
|
||||||
if (refreshInFlight) {
|
if (refreshInFlight) {
|
||||||
refreshQueued = true;
|
refreshQueued = true;
|
||||||
return refreshInFlight;
|
return refreshInFlight;
|
||||||
@@ -402,9 +405,15 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/services/status', { cache: 'no-store' });
|
const response = await fetch('/api/v1/services/status', { cache: 'no-store' });
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
// Auth lost — stop the polling loop from hammering every few seconds
|
// Auth lost — stop the polling loop and close SSE; do NOT fall
|
||||||
|
// through to direct probes (those would misleadingly mark
|
||||||
|
// services as healthy since /probe/ treats 401/403 as "up").
|
||||||
window._dcAuthLost = true;
|
window._dcAuthLost = true;
|
||||||
throw new Error(`Authentication required (${response.status})`);
|
if (window._sseReconnect && window._sseClose) {
|
||||||
|
window._sseClose(); // tell SSE to stop reconnecting
|
||||||
|
}
|
||||||
|
updateStamp('auth required');
|
||||||
|
return; // skip the fallback entirely
|
||||||
}
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status refresh failed (${response.status})`);
|
throw new Error(`Status refresh failed (${response.status})`);
|
||||||
@@ -424,9 +433,11 @@
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
refreshInFlight = null;
|
refreshInFlight = null;
|
||||||
if (refreshQueued) {
|
if (refreshQueued && !window._dcAuthLost) {
|
||||||
refreshQueued = false;
|
refreshQueued = false;
|
||||||
setTimeout(() => { window.refreshAll(); }, 0);
|
setTimeout(() => { window.refreshAll(); }, 0);
|
||||||
|
} else {
|
||||||
|
refreshQueued = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
let es = null;
|
let es = null;
|
||||||
let reconnectDelay = 1000;
|
let reconnectDelay = 1000;
|
||||||
const MAX_RECONNECT = 30000;
|
const MAX_RECONNECT = 30000;
|
||||||
|
let _sseFailCount = 0;
|
||||||
|
let _sseManuallyClosed = false;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (es) { try { es.close(); } catch (_) {} }
|
if (es) { try { es.close(); } catch (_) {} }
|
||||||
|
if (_sseManuallyClosed) return; // auth-lost: don't reconnect
|
||||||
|
|
||||||
es = new EventSource('/api/v1/events/stream');
|
es = new EventSource('/api/v1/events/stream');
|
||||||
|
|
||||||
@@ -102,11 +105,16 @@
|
|||||||
// Reconnect on error
|
// Reconnect on error
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
es.close();
|
es.close();
|
||||||
// Stop reconnecting entirely after multiple consecutive failures
|
// If auth was explicitly lost (401/403 from the polling loop),
|
||||||
// (likely auth-gate / session expired — no point hammering forever)
|
// don't attempt reconnection at all.
|
||||||
_sseFailCount = (_sseFailCount || 0) + 1;
|
if (window._dcAuthLost || _sseManuallyClosed) {
|
||||||
|
console.warn('[SSE] Auth lost — stopping reconnection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Transient failures: retry with exponential backoff, stop after 5
|
||||||
|
_sseFailCount++;
|
||||||
if (_sseFailCount > 5) {
|
if (_sseFailCount > 5) {
|
||||||
console.warn('[SSE] Max reconnect attempts reached — stopping (auth gate active or server unreachable)');
|
console.warn('[SSE] Max reconnect attempts reached — stopping (server unreachable)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn(`[SSE] Disconnected, reconnecting in ${reconnectDelay / 1000}s...`);
|
console.warn(`[SSE] Disconnected, reconnecting in ${reconnectDelay / 1000}s...`);
|
||||||
@@ -115,11 +123,16 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let _sseFailCount = 0;
|
// Called by grid.js when the polling loop detects auth loss (401/403)
|
||||||
|
function closeAndStop() {
|
||||||
|
_sseManuallyClosed = true;
|
||||||
|
if (es) { try { es.close(); } catch (_) {} }
|
||||||
|
}
|
||||||
|
|
||||||
// Start on page load
|
// Start on page load
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
// Expose for debugging
|
// Expose for debugging and cross-module coordination
|
||||||
window._sseReconnect = connect;
|
window._sseReconnect = connect;
|
||||||
|
window._sseClose = closeAndStop;
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user