/* === MeshCore Analyzer β observers.js === */
'use strict';
(function () {
let observers = [];
let wsHandler = null;
let refreshTimer = null;
let regionChangeHandler = null;
function init(app) {
app.innerHTML = `
`;
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 =
`Error loading observers: ${e.message}
`;
}
}
// 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
`;
makeColumnsResizable('#obsTable', 'meshcore-obs-col-widths');
}
registerPage('observers', { init, destroy });
})();