Files
meshcore-analyzer/public/observers.js
T
you 81f1631d16 Initial commit: MeshCore Analyzer
Bay Area MeshCore mesh network analyzer with:
- Live packet visualization with map, contrail animations, shockwave pulses
- VCR controls: pause/play/rewind/scrub timeline with speed control
- Packet browser with grouped view, detail panel, byte breakdown
- Channel message decryption (hashtag-derived PSKs)
- Node directory with health cards, favorites, search
- Analytics dashboard with network insights
- Observer management and BLE/companion bridge support
- Trace route visualization
- Dark theme, responsive design, accessibility
- SQLite storage, WebSocket live feed, REST API
2026-03-18 19:34:05 +00:00

118 lines
4.4 KiB
JavaScript

/* === MeshCore Analyzer — observers.js === */
'use strict';
(function () {
let observers = [];
let wsHandler = null;
let refreshTimer = null;
function init(app) {
app.innerHTML = `
<div class="observers-page">
<div class="page-header">
<h2>Observer Status</h2>
<button class="btn-icon" onclick="window._obsRefresh()" title="Refresh">🔄</button>
</div>
<div id="obsContent"><div class="text-center text-muted" style="padding:40px">Loading…</div></div>
</div>`;
loadObservers();
// Auto-refresh every 30s
refreshTimer = setInterval(loadObservers, 30000);
wsHandler = (msg) => {
if (msg.type === 'packet') loadObservers();
};
onWS(wsHandler);
}
function destroy() {
if (wsHandler) offWS(wsHandler);
wsHandler = null;
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = null;
observers = [];
}
async function loadObservers() {
try {
const data = await api('/observers');
observers = data.observers || [];
render();
} catch (e) {
document.getElementById('obsContent').innerHTML =
`<div class="text-muted" style="padding:40px">Error loading observers: ${e.message}</div>`;
}
}
function healthStatus(lastSeen) {
if (!lastSeen) return { cls: 'health-red', label: 'Unknown' };
const ago = Date.now() - new Date(lastSeen).getTime();
if (ago < 600000) return { cls: 'health-green', label: 'Online' }; // < 10 min
if (ago < 3600000) return { cls: 'health-yellow', label: 'Stale' }; // < 1 hour
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 '<div class="spark-bar"><div class="spark-fill" style="width:0"></div></div>';
const pct = Math.min(100, Math.round((count / max) * 100));
return `<div class="spark-bar"><div class="spark-fill" style="width:${pct}%"></div><span class="spark-label">${count}/hr</span></div>`;
}
function render() {
const el = document.getElementById('obsContent');
if (!el) return;
if (observers.length === 0) {
el.innerHTML = '<div class="text-center text-muted" style="padding:40px">No observers found.</div>';
return;
}
const maxPktsHr = Math.max(1, ...observers.map(o => o.packetsLastHour || 0));
// Summary counts
const online = observers.filter(o => healthStatus(o.last_seen).cls === 'health-green').length;
const stale = observers.filter(o => healthStatus(o.last_seen).cls === 'health-yellow').length;
const offline = observers.filter(o => healthStatus(o.last_seen).cls === 'health-red').length;
el.innerHTML = `
<div class="obs-summary">
<span class="obs-stat"><span class="health-dot health-green"></span> ${online} Online</span>
<span class="obs-stat"><span class="health-dot health-yellow"></span> ${stale} Stale</span>
<span class="obs-stat"><span class="health-dot health-red"></span> ${offline} Offline</span>
<span class="obs-stat">📡 ${observers.length} Total</span>
</div>
<table class="data-table obs-table" id="obsTable">
<thead><tr>
<th>Status</th><th>Name</th><th>Region</th><th>Last Seen</th>
<th>Packets</th><th>Packets/Hour</th><th>Uptime</th>
</tr></thead>
<tbody>${observers.map(o => {
const h = healthStatus(o.last_seen);
return `<tr>
<td><span class="health-dot ${h.cls}" title="${h.label}"></span> ${h.label}</td>
<td class="mono">${o.name || o.id}</td>
<td>${o.iata ? `<span class="badge-region">${o.iata}</span>` : '—'}</td>
<td>${timeAgo(o.last_seen)}</td>
<td>${(o.packet_count || 0).toLocaleString()}</td>
<td>${sparkBar(o.packetsLastHour || 0, maxPktsHr)}</td>
<td>${uptimeStr(o.first_seen)}</td>
</tr>`;
}).join('')}</tbody>
</table>`;
makeColumnsResizable('#obsTable', 'meshcore-obs-col-widths');
}
window._obsRefresh = loadObservers;
registerPage('observers', { init, destroy });
})();