/* === MeshCore Analyzer β€” observers.js === */ 'use strict'; (function () { let observers = []; let wsHandler = null; let refreshTimer = null; let regionChangeHandler = null; function init(app) { app.innerHTML = `
Loading…
`; RegionFilter.init(document.getElementById('obsRegionFilter')); regionChangeHandler = RegionFilter.onChange(function () { render(); }); loadObservers(); // Event delegation for data-action buttons app.addEventListener('click', function (e) { var btn = e.target.closest('[data-action]'); if (btn && btn.dataset.action === 'obs-refresh') loadObservers(); }); // Auto-refresh every 30s refreshTimer = setInterval(loadObservers, 30000); wsHandler = debouncedOnWS(function (msgs) { if (msgs.some(function (m) { return m.type === 'packet'; })) loadObservers(); }); } function destroy() { if (wsHandler) offWS(wsHandler); wsHandler = null; if (refreshTimer) clearInterval(refreshTimer); refreshTimer = null; if (regionChangeHandler) RegionFilter.offChange(regionChangeHandler); regionChangeHandler = null; observers = []; } async function loadObservers() { try { const data = await api('/observers', { ttl: CLIENT_TTL.observers }); observers = data.observers || []; render(); } catch (e) { document.getElementById('obsContent').innerHTML = ``; } } // NOTE: Comparing server timestamps to Date.now() can skew if client/server // clocks differ. We add Β±30s tolerance to thresholds to reduce false positives. function healthStatus(lastSeen) { if (!lastSeen) return { cls: 'health-red', label: 'Unknown' }; const ago = Date.now() - new Date(lastSeen).getTime(); const tolerance = 30000; // 30s tolerance for clock skew if (ago < 600000 + tolerance) return { cls: 'health-green', label: 'Online' }; // < 10 min + tolerance if (ago < 3600000 + tolerance) return { cls: 'health-yellow', label: 'Stale' }; // < 1 hour + tolerance return { cls: 'health-red', label: 'Offline' }; } function uptimeStr(firstSeen) { if (!firstSeen) return 'β€”'; const ms = Date.now() - new Date(firstSeen).getTime(); const d = Math.floor(ms / 86400000); const h = Math.floor((ms % 86400000) / 3600000); if (d > 0) return `${d}d ${h}h`; const m = Math.floor((ms % 3600000) / 60000); return h > 0 ? `${h}h ${m}m` : `${m}m`; } function sparkBar(count, max) { if (max === 0) return `0/hr`; const pct = Math.min(100, Math.round((count / max) * 100)); return `${count}/hr`; } function render() { const el = document.getElementById('obsContent'); if (!el) return; // Apply region filter const selectedRegions = RegionFilter.getSelected(); const filtered = selectedRegions ? observers.filter(o => o.iata && selectedRegions.includes(o.iata)) : observers; if (filtered.length === 0) { el.innerHTML = '
No observers found.
'; return; } const maxPktsHr = Math.max(1, ...filtered.map(o => o.packetsLastHour || 0)); // Summary counts const online = filtered.filter(o => healthStatus(o.last_seen).cls === 'health-green').length; const stale = filtered.filter(o => healthStatus(o.last_seen).cls === 'health-yellow').length; const offline = filtered.filter(o => healthStatus(o.last_seen).cls === 'health-red').length; el.innerHTML = `
● ${online} Online β–² ${stale} Stale βœ• ${offline} Offline πŸ“‘ ${filtered.length} Total
${filtered.map(o => { const h = healthStatus(o.last_seen); const shape = h.cls === 'health-green' ? '●' : h.cls === 'health-yellow' ? 'β–²' : 'βœ•'; return ``; }).join('')}
Observer status and statistics
StatusNameRegionLast Seen PacketsPackets/HourUptime
${shape} ${h.label} ${o.name || o.id} ${o.iata ? `${o.iata}` : 'β€”'} ${timeAgo(o.last_seen)} ${(o.packet_count || 0).toLocaleString()} ${sparkBar(o.packetsLastHour || 0, maxPktsHr)} ${uptimeStr(o.first_seen)}
`; makeColumnsResizable('#obsTable', 'meshcore-obs-col-widths'); } registerPage('observers', { init, destroy }); })();