// MeshDD-Bot – Paket-Log const MAX_ROWS = 300; const UNKNOWN_TYPE = '__unknown__'; let ws = null; let nodes = {}; // node_id -> {long_name, short_name, ...} let channels = {}; // ch_index -> channel name string let paused = false; let activeFilter = 'all'; let pendingRows = []; const pktBody = document.getElementById('pktBody'); const pktCount = document.getElementById('pktCount'); const pktFilterBar = document.getElementById('pktFilterBar'); const pktPauseBtn = document.getElementById('pktPauseBtn'); const pktClearBtn = document.getElementById('pktClearBtn'); // ── Portnum config ───────────────────────────────────────────── const PORTNUM_CFG = { TEXT_MESSAGE_APP: { label: 'Text', color: 'info' }, POSITION_APP: { label: 'Position', color: 'success' }, NODEINFO_APP: { label: 'NodeInfo', color: 'primary' }, TELEMETRY_APP: { label: 'Telemetry', color: 'warning' }, ROUTING_APP: { label: 'Routing', color: 'secondary' }, ADMIN_APP: { label: 'Admin', color: 'danger' }, TRACEROUTE_APP: { label: 'Traceroute', color: 'purple' }, NEIGHBORINFO_APP: { label: 'Neighbor', color: 'teal' }, RANGE_TEST_APP: { label: 'RangeTest', color: 'orange' }, [UNKNOWN_TYPE]: { label: '?', color: 'secondary' }, }; const knownTypes = new Set(); function typeKey(portnum) { return portnum || UNKNOWN_TYPE; } function typeCfg(key) { return PORTNUM_CFG[key] || { label: key.replace(/_APP$/, ''), color: 'secondary' }; } // ── Helpers ──────────────────────────────────────────────────── function nodeName(id) { if (!id) return '—'; const n = nodes[id]; if (n) { const name = n.short_name || n.long_name; if (name) return escapeHtml(name); } return escapeHtml(id); } function nodeTitle(id) { if (!id) return ''; const n = nodes[id]; if (n && (n.long_name || n.short_name)) { return `title="${escapeHtml(n.long_name || n.short_name)} (${escapeHtml(id)})"`; } return ''; } function fmtTime(ts) { if (!ts) return '—'; const d = new Date(ts * 1000); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; } function fmtTo(toId) { const broadcast = ['4294967295', '^all', 'ffffffff', '4294967295']; if (!toId || broadcast.includes(String(toId))) { return 'Alle'; } return `${nodeName(toId)}`; } function fmtChannel(ch) { if (ch == null) return '—'; const name = channels[ch]; if (name) return `${escapeHtml(name)}`; return `${ch}`; } function portnumBadge(portnum) { const key = typeKey(portnum); const cfg = typeCfg(key); return `${escapeHtml(cfg.label)}`; } function fmtPayload(portnum, payloadStr) { let p = {}; try { p = JSON.parse(payloadStr || '{}'); } catch { return ''; } if (portnum === 'TEXT_MESSAGE_APP' && p.text) return `${escapeHtml(p.text)}`; if (portnum === 'POSITION_APP' && p.lat != null) return `${p.lat?.toFixed(5)}, ${p.lon?.toFixed(5)}`; if (portnum === 'TELEMETRY_APP') { const parts = []; if (p.battery != null) parts.push(`🔋 ${p.battery}%`); if (p.voltage != null) parts.push(`${p.voltage?.toFixed(2)} V`); return parts.length ? `${parts.join(' · ')}` : ''; } if (portnum === 'NODEINFO_APP' && (p.long_name || p.short_name)) return `${escapeHtml(p.long_name || '')}${p.short_name ? ` [${escapeHtml(p.short_name)}]` : ''}`; return ''; } function fmtSnr(v) { if (v == null) return '—'; const cls = v >= 5 ? 'text-success' : v >= 0 ? 'text-warning' : 'text-danger'; return `${v > 0 ? '+' : ''}${v}`; } function fmtRssi(v) { if (v == null) return '—'; const cls = v >= -100 ? 'text-success' : v >= -115 ? 'text-warning' : 'text-danger'; return `${v}`; } function fmtHops(limit, start) { if (start == null || limit == null) return '—'; return `${start - limit}/${start}`; } // ── Filter bar ───────────────────────────────────────────────── function renderFilterBar() { const types = ['all', ...Array.from(knownTypes).sort((a, b) => { // UNKNOWN_TYPE always last if (a === UNKNOWN_TYPE) return 1; if (b === UNKNOWN_TYPE) return -1; return a.localeCompare(b); })]; pktFilterBar.innerHTML = types.map(t => { const active = t === activeFilter; if (t === 'all') { return ``; } const cfg = typeCfg(t); return ``; }).join(''); } pktFilterBar.addEventListener('click', e => { const btn = e.target.closest('.pkt-filter-btn'); if (!btn) return; activeFilter = btn.dataset.type; renderFilterBar(); applyFilter(); }); function applyFilter() { pktBody.querySelectorAll('tr[data-type]').forEach(row => { const visible = activeFilter === 'all' || row.dataset.type === activeFilter; row.classList.toggle('d-none', !visible); }); } // ── Row rendering ────────────────────────────────────────────── function buildRow(pkt) { const key = typeKey(pkt.portnum); const tr = document.createElement('tr'); tr.dataset.type = key; if (activeFilter !== 'all' && key !== activeFilter) tr.classList.add('d-none'); tr.innerHTML = `