/** * System Health – Enhanced Dashboard IIFE module * * Streams real-time system metrics via SSE with rich visualizations: * SVG arc gauge, per-core bars, temperature sparkline, network bandwidth, * disk I/O, 3D globe, weather, and process grid. */ const SystemHealth = (function () { 'use strict'; let eventSource = null; let connected = false; let lastMetrics = null; // Temperature sparkline ring buffer (last 20 readings) const SPARKLINE_SIZE = 20; let tempHistory = []; // Network I/O delta tracking let prevNetIo = null; let prevNetTimestamp = null; // Disk I/O delta tracking let prevDiskIo = null; let prevDiskTimestamp = null; // Location & weather state let locationData = null; let weatherData = null; let weatherTimer = null; let globeInstance = null; let globeDestroyed = false; const GLOBE_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/globe.gl@2.33.1/dist/globe.gl.min.js'; const GLOBE_TEXTURE_URL = '/static/images/globe/earth-dark.jpg'; // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- function formatBytes(bytes) { if (bytes == null) return '--'; var units = ['B', 'KB', 'MB', 'GB', 'TB']; var i = 0; var val = bytes; while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } return val.toFixed(1) + ' ' + units[i]; } function formatRate(bytesPerSec) { if (bytesPerSec == null) return '--'; return formatBytes(bytesPerSec) + '/s'; } function barClass(pct) { if (pct >= 85) return 'crit'; if (pct >= 60) return 'warn'; return 'ok'; } function barHtml(pct, label) { if (pct == null) return 'N/A'; var cls = barClass(pct); var rounded = Math.round(pct); return '
' + (label ? '' + label + '' : '') + '
' + '' + rounded + '%' + '
'; } function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ----------------------------------------------------------------------- // SVG Arc Gauge // ----------------------------------------------------------------------- function arcGaugeSvg(pct) { var radius = 36; var cx = 45, cy = 45; var startAngle = -225; var endAngle = 45; var totalAngle = endAngle - startAngle; // 270 degrees var fillAngle = startAngle + (totalAngle * Math.min(pct, 100) / 100); function polarToCart(angle) { var r = angle * Math.PI / 180; return { x: cx + radius * Math.cos(r), y: cy + radius * Math.sin(r) }; } var bgStart = polarToCart(startAngle); var bgEnd = polarToCart(endAngle); var fillEnd = polarToCart(fillAngle); var largeArcBg = totalAngle > 180 ? 1 : 0; var fillArc = (fillAngle - startAngle) > 180 ? 1 : 0; var cls = barClass(pct); return '' + '' + '' + ''; } // ----------------------------------------------------------------------- // Temperature Sparkline // ----------------------------------------------------------------------- function sparklineSvg(values) { if (!values || values.length < 2) return ''; var w = 200, h = 40; var min = Math.min.apply(null, values); var max = Math.max.apply(null, values); var range = max - min || 1; var step = w / (values.length - 1); var points = values.map(function (v, i) { var x = Math.round(i * step); var y = Math.round(h - ((v - min) / range) * (h - 4) - 2); return x + ',' + y; }); var areaPoints = points.join(' ') + ' ' + w + ',' + h + ' 0,' + h; return '' + '' + '' + '' + '' + '' + '' + ''; } // ----------------------------------------------------------------------- // Rendering — CPU Card // ----------------------------------------------------------------------- function renderCpuCard(m) { var el = document.getElementById('sysCardCpu'); if (!el) return; var cpu = m.cpu; if (!cpu) { el.innerHTML = '
psutil not available
'; return; } var pct = Math.round(cpu.percent); var coreHtml = ''; if (cpu.per_core && cpu.per_core.length) { coreHtml = '
'; cpu.per_core.forEach(function (c) { var cls = barClass(c); var h = Math.max(3, Math.round(c / 100 * 48)); coreHtml += '
'; }); coreHtml += '
'; } var freqHtml = ''; if (cpu.freq) { var freqGhz = (cpu.freq.current / 1000).toFixed(2); freqHtml = '
Freq: ' + freqGhz + ' GHz
'; } el.innerHTML = '
CPU
' + '
' + '
' + '
' + arcGaugeSvg(pct) + '
' + pct + '%
' + '
' + '
Load: ' + cpu.load_1 + ' / ' + cpu.load_5 + ' / ' + cpu.load_15 + '
' + '
Cores: ' + cpu.count + '
' + freqHtml + '
' + coreHtml + '
'; } // ----------------------------------------------------------------------- // Memory Card // ----------------------------------------------------------------------- function renderMemoryCard(m) { var el = document.getElementById('sysCardMemory'); if (!el) return; var mem = m.memory; if (!mem) { el.innerHTML = '
N/A
'; return; } var swap = m.swap || {}; el.innerHTML = '
Memory
' + '
' + barHtml(mem.percent, 'RAM') + '
' + formatBytes(mem.used) + ' / ' + formatBytes(mem.total) + '
' + (swap.total > 0 ? barHtml(swap.percent, 'Swap') + '
' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '
' : '') + '
'; } // ----------------------------------------------------------------------- // Temperature & Power Card // ----------------------------------------------------------------------- function _extractPrimaryTemp(temps) { if (!temps) return null; var preferred = ['cpu_thermal', 'coretemp', 'k10temp', 'acpitz', 'soc_thermal']; for (var i = 0; i < preferred.length; i++) { if (temps[preferred[i]] && temps[preferred[i]].length) return temps[preferred[i]][0]; } for (var key in temps) { if (temps[key] && temps[key].length) return temps[key][0]; } return null; } function renderTempCard(m) { var el = document.getElementById('sysCardTemp'); if (!el) return; var temp = _extractPrimaryTemp(m.temperatures); var html = '
Temperature & Power
'; if (temp) { // Update sparkline history tempHistory.push(temp.current); if (tempHistory.length > SPARKLINE_SIZE) tempHistory.shift(); html += '
' + Math.round(temp.current) + '°C
'; html += '
' + sparklineSvg(tempHistory) + '
'; // Additional sensors if (m.temperatures) { for (var chip in m.temperatures) { m.temperatures[chip].forEach(function (s) { html += '
' + escHtml(s.label) + ': ' + Math.round(s.current) + '°C
'; }); } } } else { html += 'No temperature sensors'; } // Fans if (m.fans) { for (var fChip in m.fans) { m.fans[fChip].forEach(function (f) { html += '
Fan ' + escHtml(f.label) + ': ' + f.current + ' RPM
'; }); } } // Battery if (m.battery) { html += '
' + 'Battery: ' + Math.round(m.battery.percent) + '%' + (m.battery.plugged ? ' (plugged)' : '') + '
'; } // Throttle flags (Pi) if (m.power && m.power.throttled) { html += '
Throttle: 0x' + m.power.throttled + '
'; } // Power draw if (m.power && m.power.draw_watts != null) { html += '
Power: ' + m.power.draw_watts + ' W
'; } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // Disk Card // ----------------------------------------------------------------------- function renderDiskCard(m) { var el = document.getElementById('sysCardDisk'); if (!el) return; var disk = m.disk; if (!disk) { el.innerHTML = '
Disk & Storage
N/A
'; return; } var html = '
Disk & Storage
'; html += barHtml(disk.percent, ''); html += '
' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '
'; // Disk I/O rates if (m.disk_io && prevDiskIo && prevDiskTimestamp) { var dt = (m.timestamp - prevDiskTimestamp); if (dt > 0) { var readRate = (m.disk_io.read_bytes - prevDiskIo.read_bytes) / dt; var writeRate = (m.disk_io.write_bytes - prevDiskIo.write_bytes) / dt; var readIops = Math.round((m.disk_io.read_count - prevDiskIo.read_count) / dt); var writeIops = Math.round((m.disk_io.write_count - prevDiskIo.write_count) / dt); html += '
' + 'R: ' + formatRate(Math.max(0, readRate)) + '' + 'W: ' + formatRate(Math.max(0, writeRate)) + '' + '
'; html += '
IOPS: ' + Math.max(0, readIops) + 'r / ' + Math.max(0, writeIops) + 'w
'; } } if (m.disk_io) { prevDiskIo = m.disk_io; prevDiskTimestamp = m.timestamp; } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // Network Card // ----------------------------------------------------------------------- function renderNetworkCard(m) { var el = document.getElementById('sysCardNetwork'); if (!el) return; var net = m.network; if (!net) { el.innerHTML = '
Network
N/A
'; return; } var html = '
Network
'; // Interfaces var ifaces = net.interfaces || []; if (ifaces.length === 0) { html += 'No interfaces'; } else { ifaces.forEach(function (iface) { html += '
'; html += '
' + escHtml(iface.name) + (iface.is_up ? '' : ' (down)') + '
'; if (iface.ipv4) html += '
' + escHtml(iface.ipv4) + '
'; var details = []; if (iface.mac) details.push('MAC: ' + iface.mac); if (iface.speed) details.push(iface.speed + ' Mbps'); if (details.length) html += '
' + escHtml(details.join(' | ')) + '
'; // Bandwidth for this interface if (net.io && net.io[iface.name] && prevNetIo && prevNetIo[iface.name] && prevNetTimestamp) { var dt = (m.timestamp - prevNetTimestamp); if (dt > 0) { var prev = prevNetIo[iface.name]; var cur = net.io[iface.name]; var upRate = (cur.bytes_sent - prev.bytes_sent) / dt; var downRate = (cur.bytes_recv - prev.bytes_recv) / dt; html += '
' + '↑ ' + formatRate(Math.max(0, upRate)) + '' + '↓ ' + formatRate(Math.max(0, downRate)) + '' + '
'; } } html += '
'; }); } // Connection count if (net.connections != null) { html += '
Connections: ' + net.connections + '
'; } // Save for next delta if (net.io) { prevNetIo = net.io; prevNetTimestamp = m.timestamp; } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // Location & Weather Card // ----------------------------------------------------------------------- function renderLocationCard() { var el = document.getElementById('sysCardLocation'); if (!el) return; // Preserve the globe DOM node if it already has a canvas var existingGlobe = document.getElementById('sysGlobeContainer'); var savedGlobe = null; if (existingGlobe && existingGlobe.querySelector('canvas')) { savedGlobe = existingGlobe; existingGlobe.parentNode.removeChild(existingGlobe); } var html = '
Location & Weather
'; html += '
'; // Globe placeholder (will be replaced with saved node or initialized fresh) if (!savedGlobe) { html += '
'; } else { html += '
'; } // Details below globe html += '
'; if (locationData && locationData.lat != null) { html += '
' + locationData.lat.toFixed(4) + '°' + (locationData.lat >= 0 ? 'N' : 'S') + ', ' + locationData.lon.toFixed(4) + '°' + (locationData.lon >= 0 ? 'E' : 'W') + '
'; // GPS status indicator if (locationData.source === 'gps' && locationData.gps) { var gps = locationData.gps; var fixLabel = gps.fix_quality === 3 ? '3D Fix' : '2D Fix'; var dotCls = gps.fix_quality === 3 ? 'fix-3d' : 'fix-2d'; html += '
' + ' ' + fixLabel; if (gps.satellites != null) html += ' · ' + gps.satellites + ' sats'; if (gps.accuracy != null) html += ' · ±' + gps.accuracy + 'm'; html += '
'; } else { html += '
Source: ' + escHtml(locationData.source || 'unknown') + '
'; } } else { html += '
No location
'; } // Weather if (weatherData && !weatherData.error) { html += '
'; html += '
' + (weatherData.temp_c || '--') + '°C
'; html += '
' + escHtml(weatherData.condition || '') + '
'; var details = []; if (weatherData.humidity) details.push('Humidity: ' + weatherData.humidity + '%'); if (weatherData.wind_mph) details.push('Wind: ' + weatherData.wind_mph + ' mph ' + (weatherData.wind_dir || '')); if (weatherData.feels_like_c) details.push('Feels like: ' + weatherData.feels_like_c + '°C'); details.forEach(function (d) { html += '
' + escHtml(d) + '
'; }); html += '
'; } else if (weatherData && weatherData.error) { html += '
Weather unavailable
'; } html += '
'; // .sys-location-details html += '
'; // .sys-location-inner html += '
'; el.innerHTML = html; // Re-insert saved globe or initialize fresh if (savedGlobe) { var placeholder = document.getElementById('sysGlobePlaceholder'); if (placeholder) placeholder.parentNode.replaceChild(savedGlobe, placeholder); } else { requestAnimationFrame(function () { initGlobe(); }); } } // ----------------------------------------------------------------------- // Globe (reuses globe.gl from GPS mode) // ----------------------------------------------------------------------- function ensureGlobeLibrary() { return new Promise(function (resolve, reject) { if (typeof window.Globe === 'function') { resolve(true); return; } // Check if script already exists var existing = document.querySelector( 'script[data-intercept-globe-src="' + GLOBE_SCRIPT_URL + '"], ' + 'script[src="' + GLOBE_SCRIPT_URL + '"]' ); if (existing) { if (existing.dataset.loaded === 'true') { resolve(true); return; } if (existing.dataset.failed === 'true') { resolve(false); return; } existing.addEventListener('load', function () { resolve(true); }, { once: true }); existing.addEventListener('error', function () { resolve(false); }, { once: true }); return; } var script = document.createElement('script'); script.src = GLOBE_SCRIPT_URL; script.async = true; script.crossOrigin = 'anonymous'; script.dataset.interceptGlobeSrc = GLOBE_SCRIPT_URL; script.onload = function () { script.dataset.loaded = 'true'; resolve(true); }; script.onerror = function () { script.dataset.failed = 'true'; resolve(false); }; document.head.appendChild(script); }); } function initGlobe() { var container = document.getElementById('sysGlobeContainer'); if (!container || globeDestroyed) return; // Don't reinitialize if globe canvas is still alive in this container if (globeInstance && container.querySelector('canvas')) return; // Clear stale reference if canvas was destroyed by innerHTML replacement if (globeInstance && !container.querySelector('canvas')) { globeInstance = null; } ensureGlobeLibrary().then(function (ready) { if (!ready || typeof window.Globe !== 'function' || globeDestroyed) return; // Wait for layout — container may have 0 dimensions right after // display:none is removed by switchMode(). Use RAF retry like GPS mode. var attempts = 0; function tryInit() { if (globeDestroyed) return; container = document.getElementById('sysGlobeContainer'); if (!container) return; if ((!container.clientWidth || !container.clientHeight) && attempts < 8) { attempts++; requestAnimationFrame(tryInit); return; } if (!container.clientWidth || !container.clientHeight) return; container.innerHTML = ''; container.style.background = 'radial-gradient(circle, rgba(10,20,40,0.9), rgba(2,4,8,0.98) 70%)'; try { globeInstance = window.Globe()(container) .backgroundColor('rgba(0,0,0,0)') .globeImageUrl(GLOBE_TEXTURE_URL) .showAtmosphere(true) .atmosphereColor('#3bb9ff') .atmosphereAltitude(0.12) .pointsData([]) .pointRadius(0.8) .pointAltitude(0.01) .pointColor(function () { return '#00d4ff'; }); var controls = globeInstance.controls(); if (controls) { controls.autoRotate = true; controls.autoRotateSpeed = 0.5; controls.enablePan = false; controls.minDistance = 120; controls.maxDistance = 300; } // Size the globe globeInstance.width(container.clientWidth); globeInstance.height(container.clientHeight); updateGlobePosition(); } catch (e) { // Globe.gl / WebGL init failed — show static fallback container.innerHTML = '
Globe unavailable
'; } } requestAnimationFrame(tryInit); }); } function updateGlobePosition() { if (!globeInstance || !locationData || locationData.lat == null) return; // Observer point globeInstance.pointsData([{ lat: locationData.lat, lng: locationData.lon, size: 0.8, color: '#00d4ff', }]); // Snap view globeInstance.pointOfView({ lat: locationData.lat, lng: locationData.lon, altitude: 2.0 }, 1000); // Stop auto-rotate when we have a fix var controls = globeInstance.controls(); if (controls) controls.autoRotate = false; } function destroyGlobe() { globeDestroyed = true; if (globeInstance) { var container = document.getElementById('sysGlobeContainer'); if (container) container.innerHTML = ''; globeInstance = null; } } // ----------------------------------------------------------------------- // SDR Card // ----------------------------------------------------------------------- function renderSdrCard(devices) { var el = document.getElementById('sysCardSdr'); if (!el) return; var html = '
SDR Devices
'; html += '
'; if (!devices || !devices.length) { html += 'No devices found'; } else { devices.forEach(function (d) { html += '
' + ' ' + '' + escHtml(d.type) + ' #' + d.index + '' + '
' + escHtml(d.name || 'Unknown') + '
' + (d.serial ? '
S/N: ' + escHtml(d.serial) + '
' : '') + '
'; }); } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // Process Card // ----------------------------------------------------------------------- function renderProcessCard(m) { var el = document.getElementById('sysCardProcesses'); if (!el) return; var procs = m.processes || {}; var keys = Object.keys(procs).sort(); var html = '
Active Processes
'; if (!keys.length) { html += 'No data'; } else { var running = 0, stopped = 0; html += '
'; keys.forEach(function (k) { var isRunning = procs[k]; if (isRunning) running++; else stopped++; var dotCls = isRunning ? 'running' : 'stopped'; var label = k.charAt(0).toUpperCase() + k.slice(1); html += '
' + ' ' + '' + escHtml(label) + '' + '
'; }); html += '
'; html += '
' + running + ' running / ' + stopped + ' idle
'; } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // System Info Card // ----------------------------------------------------------------------- function renderSystemInfoCard(m) { var el = document.getElementById('sysCardInfo'); if (!el) return; var sys = m.system || {}; var html = '
System Info
'; html += '
Host' + escHtml(sys.hostname || '--') + '
'; html += '
OS' + escHtml((sys.platform || '--').replace(/-with-glibc[\d.]+/, '')) + '
'; html += '
Python' + escHtml(sys.python || '--') + '
'; html += '
Appv' + escHtml(sys.version || '--') + '
'; html += '
Uptime' + escHtml(sys.uptime_human || '--') + '
'; if (m.boot_time) { var bootDate = new Date(m.boot_time * 1000); html += '
Boot' + escHtml(bootDate.toLocaleString()) + '
'; } if (m.network && m.network.connections != null) { html += '
Connections' + m.network.connections + '
'; } html += '
'; el.innerHTML = html; } // ----------------------------------------------------------------------- // Sidebar Updates // ----------------------------------------------------------------------- function updateSidebarQuickStats(m) { var cpuEl = document.getElementById('sysQuickCpu'); var tempEl = document.getElementById('sysQuickTemp'); var ramEl = document.getElementById('sysQuickRam'); var diskEl = document.getElementById('sysQuickDisk'); if (cpuEl) cpuEl.textContent = m.cpu ? Math.round(m.cpu.percent) + '%' : '--'; if (ramEl) ramEl.textContent = m.memory ? Math.round(m.memory.percent) + '%' : '--'; if (diskEl) diskEl.textContent = m.disk ? Math.round(m.disk.percent) + '%' : '--'; var temp = _extractPrimaryTemp(m.temperatures); if (tempEl) tempEl.innerHTML = temp ? Math.round(temp.current) + '°C' : '--'; // Color-code values [cpuEl, ramEl, diskEl].forEach(function (el) { if (!el) return; var val = parseInt(el.textContent); el.classList.remove('sys-val-ok', 'sys-val-warn', 'sys-val-crit'); if (!isNaN(val)) el.classList.add('sys-val-' + barClass(val)); }); } function updateSidebarProcesses(m) { var el = document.getElementById('sysProcessList'); if (!el) return; var procs = m.processes || {}; var keys = Object.keys(procs).sort(); if (!keys.length) { el.textContent = 'No data'; return; } var running = keys.filter(function (k) { return procs[k]; }); var stopped = keys.filter(function (k) { return !procs[k]; }); el.innerHTML = (running.length ? '' + running.length + ' running' : '') + (running.length && stopped.length ? ' · ' : '') + (stopped.length ? '' + stopped.length + ' stopped' : ''); } function updateSidebarNetwork(m) { var el = document.getElementById('sysQuickNet'); if (!el || !m.network) return; var ifaces = m.network.interfaces || []; var ips = []; ifaces.forEach(function (iface) { if (iface.ipv4 && iface.is_up) { ips.push(iface.name + ': ' + iface.ipv4); } }); el.textContent = ips.length ? ips.join(', ') : '--'; } function updateSidebarBattery(m) { var section = document.getElementById('sysQuickBatterySection'); var el = document.getElementById('sysQuickBattery'); if (!section || !el) return; if (m.battery) { section.style.display = ''; el.textContent = Math.round(m.battery.percent) + '%' + (m.battery.plugged ? ' (plugged)' : ''); } else { section.style.display = 'none'; } } function updateSidebarLocation() { var el = document.getElementById('sysQuickLocation'); if (!el) return; if (locationData && locationData.lat != null) { el.textContent = locationData.lat.toFixed(4) + ', ' + locationData.lon.toFixed(4) + ' (' + locationData.source + ')'; } else { el.textContent = 'No location'; } } // ----------------------------------------------------------------------- // Render all // ----------------------------------------------------------------------- function renderAll(m) { renderCpuCard(m); renderMemoryCard(m); renderTempCard(m); renderDiskCard(m); renderNetworkCard(m); renderProcessCard(m); renderSystemInfoCard(m); updateSidebarQuickStats(m); updateSidebarProcesses(m); updateSidebarNetwork(m); updateSidebarBattery(m); } // ----------------------------------------------------------------------- // Location & Weather Fetching // ----------------------------------------------------------------------- function fetchLocation() { fetch('/system/location') .then(function (r) { return r.json(); }) .then(function (data) { // If server only has default/none, check client-side saved location if ((data.source === 'default' || data.source === 'none') && window.ObserverLocation && ObserverLocation.getShared) { var shared = ObserverLocation.getShared(); if (shared && shared.lat && shared.lon) { data.lat = shared.lat; data.lon = shared.lon; data.source = 'manual'; } } locationData = data; updateSidebarLocation(); renderLocationCard(); if (data.lat != null) fetchWeather(); }) .catch(function () { renderLocationCard(); }); } function fetchWeather() { if (!locationData || locationData.lat == null) return; fetch('/system/weather?lat=' + locationData.lat + '&lon=' + locationData.lon) .then(function (r) { return r.json(); }) .then(function (data) { weatherData = data; renderLocationCard(); }) .catch(function () {}); } // ----------------------------------------------------------------------- // SSE Connection // ----------------------------------------------------------------------- function connect() { if (eventSource) return; eventSource = new EventSource('/system/stream'); eventSource.onmessage = function (e) { try { var data = JSON.parse(e.data); if (data.type === 'keepalive') return; lastMetrics = data; renderAll(data); } catch (_) { /* ignore parse errors */ } }; eventSource.onopen = function () { connected = true; }; eventSource.onerror = function () { connected = false; }; } function disconnect() { if (eventSource) { eventSource.close(); eventSource = null; } connected = false; } // ----------------------------------------------------------------------- // SDR Devices // ----------------------------------------------------------------------- function refreshSdr() { var sidebarEl = document.getElementById('sysSdrList'); if (sidebarEl) sidebarEl.innerHTML = 'Scanning…'; var cardEl = document.getElementById('sysCardSdr'); if (cardEl) cardEl.innerHTML = '
SDR Devices
Scanning…
'; fetch('/system/sdr_devices') .then(function (r) { return r.json(); }) .then(function (data) { var devices = data.devices || []; renderSdrCard(devices); // Update sidebar if (sidebarEl) { if (!devices.length) { sidebarEl.innerHTML = 'No SDR devices found'; } else { var html = ''; devices.forEach(function (d) { html += '
' + escHtml(d.type) + ' #' + d.index + ' — ' + escHtml(d.name || 'Unknown') + '
'; }); sidebarEl.innerHTML = html; } } }) .catch(function () { if (sidebarEl) sidebarEl.innerHTML = 'Detection failed'; renderSdrCard([]); }); } // ----------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------- function init() { globeDestroyed = false; connect(); refreshSdr(); fetchLocation(); // Refresh weather every 10 minutes weatherTimer = setInterval(function () { fetchWeather(); }, 600000); } function destroy() { disconnect(); destroyGlobe(); if (weatherTimer) { clearInterval(weatherTimer); weatherTimer = null; } } return { init: init, destroy: destroy, refreshSdr: refreshSdr, }; })();