diff --git a/public/js/app.js b/public/js/app.js index 1623908..051bd43 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -111,6 +111,8 @@ // ---- State ---- let previousMetrics = null; let networkChart = null; + let ws = null; // WebSocket instance + let isWsConnecting = false; // Connection lock let user = null; // Currently logged in user let currentServerDetail = { instance: null, job: null, source: null, charts: {} }; let allServersData = []; @@ -119,6 +121,10 @@ let pageSize = 20; let currentLatencies = []; // Array of {id, source, dest, latency} let latencyTimer = null; + let mapResizeHandler = null; // For map cleanup + let siteThemeQuery = null; // For media query cleanup + let siteThemeHandler = null; + let backgroundIntervals = []; // To track setIntervals // Load sort state from localStorage or use default let currentSort = { column: 'up', direction: 'desc' }; @@ -357,28 +363,42 @@ // Still populate inputs dom.pageNameInput.value = window.SITE_SETTINGS.page_name || ''; - dom.siteTitleInput.value = window.SITE_SETTINGS.title || ''; - dom.logoUrlInput.value = window.SITE_SETTINGS.logo_url || ''; - dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark'; - dom.show95BandwidthInput.value = window.SITE_SETTINGS.show_95_bandwidth ? "1" : "0"; - dom.p95TypeSelect.value = window.SITE_SETTINGS.p95_type || 'tx'; + if (dom.siteTitleInput) dom.siteTitleInput.value = window.SITE_SETTINGS.title || ''; + if (dom.logoUrlInput) dom.logoUrlInput.value = window.SITE_SETTINGS.logo_url || ''; + if (dom.defaultThemeInput) dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark'; + if (dom.show95BandwidthInput) dom.show95BandwidthInput.value = window.SITE_SETTINGS.show_95_bandwidth ? "1" : "0"; + if (dom.p95TypeSelect) dom.p95TypeSelect.value = window.SITE_SETTINGS.p95_type || 'tx'; // Latency routes loaded separately in openSettings or on startup } loadSiteSettings(); - // setInterval(fetchMetrics, REFRESH_INTERVAL); - Now using WebSockets + // Track intervals for resource management initWebSocket(); - setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL); - fetchLatency(); - setInterval(fetchLatency, REFRESH_INTERVAL); + backgroundIntervals.push(setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL)); + backgroundIntervals.push(setInterval(fetchLatency, REFRESH_INTERVAL)); } // ---- Real-time WebSocket ---- function initWebSocket() { + if (isWsConnecting) return; + isWsConnecting = true; + + if (ws) { + ws.onmessage = null; + ws.onclose = null; + ws.onerror = null; + ws.close(); + } + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}`; - const ws = new WebSocket(wsUrl); + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + isWsConnecting = false; + console.log('WS connection established'); + }; ws.onmessage = (event) => { try { @@ -394,12 +414,13 @@ }; ws.onclose = () => { + isWsConnecting = false; console.log('WS connection closed. Reconnecting in 5s...'); setTimeout(initWebSocket, 5000); }; ws.onerror = (err) => { - // Small silent error here to not alert the user constantly if server is down during maintenance + isWsConnecting = false; ws.close(); }; } @@ -597,6 +618,13 @@ echarts.registerMap('world', worldJSON); + if (myMap2D) { + myMap2D.dispose(); + if (mapResizeHandler) { + window.removeEventListener('resize', mapResizeHandler); + } + } + myMap2D = echarts.init(dom.globeContainer); const isLight = document.documentElement.classList.contains('light-theme'); @@ -663,7 +691,10 @@ myMap2D.setOption(option); - window.addEventListener('resize', () => myMap2D.resize()); + mapResizeHandler = debounce(() => { + if (myMap2D) myMap2D.resize(); + }, 100); + window.addEventListener('resize', mapResizeHandler); if (allServersData.length > 0) { updateMap2D(allServersData); @@ -1155,6 +1186,13 @@ // ---- Server Detail ---- async function openServerDetail(instance, job, source) { + // Cleanup old charts if any were still present from a previous open (safety) + if (currentServerDetail.charts) { + Object.values(currentServerDetail.charts).forEach(chart => { + if (chart && chart.destroy) chart.destroy(); + }); + } + currentServerDetail = { instance, job, source, charts: {} }; dom.serverDetailTitle.textContent = `${job}`; dom.serverDetailModal.classList.add('active'); @@ -1505,15 +1543,21 @@ const themeToApply = savedTheme || settings.default_theme || 'dark'; applyTheme(themeToApply); - // Listen for system theme changes if set to auto - window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { + // Listen for system theme changes if set to auto (cleanup existing listener first) + if (siteThemeQuery && siteThemeHandler) { + siteThemeQuery.removeEventListener('change', siteThemeHandler); + } + + siteThemeQuery = window.matchMedia('(prefers-color-scheme: light)'); + siteThemeHandler = () => { const currentSavedTheme = localStorage.getItem('theme'); const defaultTheme = window.SITE_SETTINGS ? window.SITE_SETTINGS.default_theme : 'dark'; const activeTheme = currentSavedTheme || defaultTheme; if (activeTheme === 'auto') { applyTheme('auto'); } - }); + }; + siteThemeQuery.addEventListener('change', siteThemeHandler); } catch (err) { console.error('Error loading site settings:', err); } diff --git a/public/js/chart.js b/public/js/chart.js index e525f2e..2d8f5af 100644 --- a/public/js/chart.js +++ b/public/js/chart.js @@ -16,10 +16,11 @@ class AreaChart { this.dpr = window.devicePixelRatio || 1; this.padding = { top: 20, right: 16, bottom: 32, left: 56 }; - this.currentMaxVal = 0; this.prevMaxVal = 0; + this.currentMaxVal = 0; - this._resize = this.resize.bind(this); + // Use debounced resize for performance and safety + this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this); window.addEventListener('resize', this._resize); this.resize(); } @@ -335,7 +336,8 @@ class MetricChart { this.prevMaxVal = 0; this.currentMaxVal = 0; - this._resize = this.resize.bind(this); + // Use debounced resize for performance and safety + this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this); window.addEventListener('resize', this._resize); this.resize(); } diff --git a/public/js/utils.js b/public/js/utils.js index 42e48f0..fd7200d 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -111,3 +111,17 @@ function animateValue(element, start, end, duration = 600) { requestAnimationFrame(update); } + +/** + * Debounce function to limit execution frequency + */ +function debounce(fn, delay) { + let timer = null; + return function (...args) { + if (timer) clearTimeout(timer); + timer = setTimeout(() => { + fn.apply(this, args); + timer = null; + }, delay); + }; +}