Handle transient network suspension in frontend polling and SSE

This commit is contained in:
Smittix
2026-02-24 22:25:59 +00:00
parent 636ca84282
commit 2ef6dfe472
5 changed files with 78 additions and 7 deletions
+9 -1
View File
@@ -7,6 +7,7 @@ const AlertCenter = (function() {
let rules = []; let rules = [];
let eventSource = null; let eventSource = null;
let reconnectTimer = null; let reconnectTimer = null;
let lastConnectionWarningAt = 0;
function init() { function init() {
loadRules(); loadRules();
@@ -31,7 +32,14 @@ const AlertCenter = (function() {
}; };
eventSource.onerror = function() { eventSource.onerror = function() {
console.warn('[Alerts] SSE connection error'); const now = Date.now();
const offline = (typeof window.isOffline === 'function' && window.isOffline()) ||
(typeof navigator !== 'undefined' && navigator.onLine === false);
const shouldLog = !offline && !document.hidden && (now - lastConnectionWarningAt) > 15000;
if (shouldLog) {
lastConnectionWarningAt = now;
console.warn('[Alerts] SSE connection error; retrying');
}
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2500); reconnectTimer = setTimeout(connect, 2500);
}; };
+13 -1
View File
@@ -92,8 +92,9 @@ const RunState = (function() {
renderHealth(data); renderHealth(data);
} catch (err) { } catch (err) {
renderHealth(null, err); renderHealth(null, err);
const transient = isTransientFailure(err);
const now = Date.now(); const now = Date.now();
if (typeof reportActionableError === 'function' && (now - lastErrorToastAt) > 30000) { if (!transient && typeof reportActionableError === 'function' && (now - lastErrorToastAt) > 30000) {
lastErrorToastAt = now; lastErrorToastAt = now;
reportActionableError('Run State', err, { persistent: false }); reportActionableError('Run State', err, { persistent: false });
} }
@@ -214,6 +215,17 @@ const RunState = (function() {
return String(err); return String(err);
} }
function isTransientFailure(err) {
if (typeof window.isTransientOrOffline === 'function' && window.isTransientOrOffline(err)) {
return true;
}
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
return true;
}
const text = extractMessage(err).toLowerCase();
return text.includes('failed to fetch') || text.includes('network') || text.includes('timeout');
}
function getLastHealth() { function getLastHealth() {
return lastHealth; return lastHealth;
} }
+39 -2
View File
@@ -208,9 +208,31 @@ const AppFeedback = (function() {
return state; return state;
} }
function isOffline() {
return typeof navigator !== 'undefined' && navigator.onLine === false;
}
function isTransientNetworkError(error) {
const text = String(extractMessage(error) || '').toLowerCase();
if (!text) return false;
return text.includes('networkerror') ||
text.includes('failed to fetch') ||
text.includes('network request failed') ||
text.includes('load failed') ||
text.includes('err_network_io_suspended') ||
text.includes('network io suspended') ||
text.includes('the network connection was lost') ||
text.includes('connection reset') ||
text.includes('timeout');
}
function isTransientOrOffline(error) {
return isOffline() || isTransientNetworkError(error);
}
function isNetworkError(message) { function isNetworkError(message) {
const text = String(message || '').toLowerCase(); return isTransientNetworkError(message);
return text.includes('networkerror') || text.includes('failed to fetch') || text.includes('timeout');
} }
function isSettingsError(message) { function isSettingsError(message) {
@@ -224,6 +246,9 @@ const AppFeedback = (function() {
reportError, reportError,
removeToast, removeToast,
renderCollectionState, renderCollectionState,
isOffline,
isTransientNetworkError,
isTransientOrOffline,
}; };
})(); })();
@@ -243,6 +268,18 @@ window.renderCollectionState = function(container, options) {
return AppFeedback.renderCollectionState(container, options); return AppFeedback.renderCollectionState(container, options);
}; };
window.isOffline = function() {
return AppFeedback.isOffline();
};
window.isTransientNetworkError = function(error) {
return AppFeedback.isTransientNetworkError(error);
};
window.isTransientOrOffline = function(error) {
return AppFeedback.isTransientOrOffline(error);
};
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
AppFeedback.init(); AppFeedback.init();
}); });
+11 -2
View File
@@ -5655,10 +5655,19 @@
renderSdrStatus(devices); renderSdrStatus(devices);
}) })
.catch(err => { .catch(err => {
console.error('Failed to fetch SDR status:', err); const transient = (typeof window.isTransientOrOffline === 'function' && window.isTransientOrOffline(err)) ||
(typeof navigator !== 'undefined' && navigator.onLine === false) ||
/failed to fetch|network io suspended|networkerror|timeout/i.test(String((err && err.message) || err || ''));
if (!transient) {
console.error('Failed to fetch SDR status:', err);
}
const container = document.getElementById('sdrStatusList'); const container = document.getElementById('sdrStatusList');
if (container) { if (container) {
container.innerHTML = '<div style="padding: 8px; color: #ff6666; font-size: 11px; text-align: center;">Error loading status</div>'; if (transient) {
container.innerHTML = '<div style="padding: 8px; color: #888; font-size: 11px; text-align: center;">Status temporarily unavailable</div>';
} else {
container.innerHTML = '<div style="padding: 8px; color: #ff6666; font-size: 11px; text-align: center;">Error loading status</div>';
}
} }
}); });
} }
+6 -1
View File
@@ -971,7 +971,12 @@
} }
} }
} catch (err) { } catch (err) {
console.error('Position update error:', err); const transient = (typeof window.isTransientOrOffline === 'function' && window.isTransientOrOffline(err)) ||
(typeof navigator !== 'undefined' && navigator.onLine === false) ||
/failed to fetch|network io suspended|networkerror|timeout/i.test(String((err && err.message) || err || ''));
if (!transient) {
console.error('Position update error:', err);
}
} }
} }