diff --git a/static/js/core/alerts.js b/static/js/core/alerts.js index e5cc50a..dee24bb 100644 --- a/static/js/core/alerts.js +++ b/static/js/core/alerts.js @@ -8,16 +8,41 @@ const AlertCenter = (function() { let eventSource = null; let reconnectTimer = null; let lastConnectionWarningAt = 0; + let rulesLoaded = false; + let rulesPromise = null; + let bootTimer = null; + let feedLoaded = false; - function init() { - loadRules(); - loadFeed(); - connect(); + function init(options = {}) { + const connectFeed = options.connectFeed !== false; + const refreshRules = options.refreshRules === true; + + if (bootTimer) { + clearTimeout(bootTimer); + bootTimer = null; + } + + loadRules(refreshRules); + + if (connectFeed) { + if (!feedLoaded) { + loadFeed(); + } + connect(); + } + } + + function scheduleInit(delayMs = 15000) { + if (bootTimer || eventSource) return; + bootTimer = window.setTimeout(() => { + bootTimer = null; + init(); + }, delayMs); } function connect() { if (eventSource) { - eventSource.close(); + return; } eventSource = new EventSource('/alerts/stream'); @@ -40,6 +65,10 @@ const AlertCenter = (function() { lastConnectionWarningAt = now; console.warn('[Alerts] SSE connection error; retrying'); } + if (eventSource) { + eventSource.close(); + eventSource = null; + } if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, 2500); }; @@ -133,6 +162,7 @@ const AlertCenter = (function() { } function loadFeed() { + feedLoaded = true; fetch('/alerts/events?limit=30') .then((r) => r.json()) .then((data) => { @@ -144,21 +174,37 @@ const AlertCenter = (function() { .catch((err) => console.error('[Alerts] Load feed failed', err)); } - function loadRules() { - return fetch('/alerts/rules?all=1') + function loadRules(force = false) { + if (!force && rulesLoaded) { + renderRulesUI(); + return Promise.resolve(rules); + } + if (!force && rulesPromise) { + return rulesPromise; + } + + rulesPromise = fetch('/alerts/rules?all=1') .then((r) => r.json()) .then((data) => { if (data.status === 'success') { rules = data.rules || []; + rulesLoaded = true; renderRulesUI(); } + return rules; }) .catch((err) => { console.error('[Alerts] Load rules failed', err); if (typeof reportActionableError === 'function') { reportActionableError('Alert Rules', err, { onRetry: loadRules }); } + throw err; + }) + .finally(() => { + rulesPromise = null; }); + + return rulesPromise; } function saveRule() { @@ -260,7 +306,7 @@ const AlertCenter = (function() { if (data.status !== 'success') { throw new Error(data.message || 'Failed to update rule'); } - return loadRules(); + return loadRules(true); }) .catch((err) => { if (typeof reportActionableError === 'function') { @@ -287,7 +333,7 @@ const AlertCenter = (function() { if (Number(getEditingRuleId()) === Number(ruleId)) { clearRuleForm(); } - return loadRules(); + return loadRules(true); }) .catch((err) => { if (typeof reportActionableError === 'function') { @@ -325,7 +371,7 @@ const AlertCenter = (function() { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }), - }).then(() => loadRules()); + }).then(() => loadRules(true)); } if (enabled) { @@ -341,7 +387,7 @@ const AlertCenter = (function() { enabled: true, notify: { webhook: true }, }), - }).then(() => loadRules()); + }).then(() => loadRules(true)); } return null; }); @@ -349,41 +395,63 @@ const AlertCenter = (function() { function addBluetoothWatchlist(address, name) { if (!address) return; - const upper = String(address).toUpperCase(); - const existing = rules.find((r) => r.mode === 'bluetooth' && r.match && String(r.match.address || '').toUpperCase() === upper); - if (existing) return; + loadRules().then(() => { + const upper = String(address).toUpperCase(); + const existing = rules.find((r) => r.mode === 'bluetooth' && r.match && String(r.match.address || '').toUpperCase() === upper); + if (existing) return; - fetch('/alerts/rules', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: name ? `Watchlist ${name}` : `Watchlist ${upper}`, - mode: 'bluetooth', - event_type: 'device_update', - match: { address: upper }, - severity: 'medium', - enabled: true, - notify: { webhook: true }, - }), - }).then(() => loadRules()); + return fetch('/alerts/rules', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name ? `Watchlist ${name}` : `Watchlist ${upper}`, + mode: 'bluetooth', + event_type: 'device_update', + match: { address: upper }, + severity: 'medium', + enabled: true, + notify: { webhook: true }, + }), + }).then(() => loadRules(true)); + }); } function removeBluetoothWatchlist(address) { if (!address) return; - const upper = String(address).toUpperCase(); - const existing = rules.find((r) => r.mode === 'bluetooth' && r.match && String(r.match.address || '').toUpperCase() === upper); - if (!existing) return; + loadRules().then(() => { + const upper = String(address).toUpperCase(); + const existing = rules.find((r) => r.mode === 'bluetooth' && r.match && String(r.match.address || '').toUpperCase() === upper); + if (!existing) return; - fetch(`/alerts/rules/${existing.id}`, { method: 'DELETE' }) - .then(() => loadRules()); + return fetch(`/alerts/rules/${existing.id}`, { method: 'DELETE' }) + .then(() => loadRules(true)); + }); } function isWatchlisted(address) { if (!address) return false; + if (!rulesLoaded && !rulesPromise) { + loadRules(); + } const upper = String(address).toUpperCase(); return rules.some((r) => r.mode === 'bluetooth' && r.match && String(r.match.address || '').toUpperCase() === upper && r.enabled); } + function destroy() { + if (bootTimer) { + clearTimeout(bootTimer); + bootTimer = null; + } + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (eventSource) { + eventSource.close(); + eventSource = null; + } + } + function escapeHtml(str) { if (!str) return ''; return String(str) @@ -396,6 +464,7 @@ const AlertCenter = (function() { return { init, + scheduleInit, loadFeed, loadRules, saveRule, @@ -408,11 +477,12 @@ const AlertCenter = (function() { addBluetoothWatchlist, removeBluetoothWatchlist, isWatchlisted, + destroy, }; })(); document.addEventListener('DOMContentLoaded', () => { if (typeof AlertCenter !== 'undefined') { - AlertCenter.init(); + AlertCenter.scheduleInit(); } }); diff --git a/static/js/core/recordings.js b/static/js/core/recordings.js index 8f6fa65..2f5257f 100644 --- a/static/js/core/recordings.js +++ b/static/js/core/recordings.js @@ -137,9 +137,3 @@ const RecordingUI = (function() { openReplay, }; })(); - -document.addEventListener('DOMContentLoaded', () => { - if (typeof RecordingUI !== 'undefined') { - RecordingUI.init(); - } -}); diff --git a/static/js/core/settings-manager.js b/static/js/core/settings-manager.js index 42d22d7..0449603 100644 --- a/static/js/core/settings-manager.js +++ b/static/js/core/settings-manager.js @@ -1292,11 +1292,11 @@ function switchSettingsTab(tabName) { } else if (tabName === 'alerts') { loadVoiceAlertConfig(); if (typeof AlertCenter !== 'undefined') { - AlertCenter.loadFeed(); + AlertCenter.init(); } } else if (tabName === 'recording') { if (typeof RecordingUI !== 'undefined') { - RecordingUI.refresh(); + RecordingUI.init(); } } else if (tabName === 'apikeys') { loadApiKeyStatus(); diff --git a/static/js/core/updater.js b/static/js/core/updater.js index 3f4eb25..5153d6d 100644 --- a/static/js/core/updater.js +++ b/static/js/core/updater.js @@ -2,12 +2,13 @@ * Updater Module - GitHub update checking and notification system */ -const Updater = { - // State - _checkInterval: null, - _toastElement: null, - _modalElement: null, - _updateData: null, +const Updater = { + // State + _checkInterval: null, + _startupCheckTimer: null, + _toastElement: null, + _modalElement: null, + _updateData: null, // Configuration CHECK_INTERVAL_MS: 6 * 60 * 60 * 1000, // 6 hours in milliseconds @@ -15,18 +16,31 @@ const Updater = { /** * Initialize the updater module */ - init() { - // Create toast container if it doesn't exist - this._ensureToastContainer(); - - // Check for updates on page load - this.checkForUpdates(); - - // Set up periodic checks - this._checkInterval = setInterval(() => { - this.checkForUpdates(); - }, this.CHECK_INTERVAL_MS); - }, + init() { + // Create toast container if it doesn't exist + this._ensureToastContainer(); + + const enabled = localStorage.getItem('intercept_update_check_enabled') !== 'false'; + if (!enabled) { + this.destroy(); + return; + } + + // Defer the first check so the active dashboard can finish loading first. + if (!this._startupCheckTimer) { + this._startupCheckTimer = setTimeout(() => { + this._startupCheckTimer = null; + this.checkForUpdates(); + }, 15000); + } + + // Set up periodic checks + if (!this._checkInterval) { + this._checkInterval = setInterval(() => { + this.checkForUpdates(); + }, this.CHECK_INTERVAL_MS); + } + }, /** * Ensure toast container exists in DOM @@ -505,11 +519,15 @@ const Updater = { /** * Clean up on page unload */ - destroy() { - if (this._checkInterval) { - clearInterval(this._checkInterval); - this._checkInterval = null; - } + destroy() { + if (this._startupCheckTimer) { + clearTimeout(this._startupCheckTimer); + this._startupCheckTimer = null; + } + if (this._checkInterval) { + clearInterval(this._checkInterval); + this._checkInterval = null; + } this.hideToast(); this.hideModal(); } diff --git a/templates/index.html b/templates/index.html index 34a3576..937be01 100644 --- a/templates/index.html +++ b/templates/index.html @@ -16241,40 +16241,6 @@