// MeshDD-Bot – Paket-Log const MAX_ROWS = 300; let ws = null; let nodes = {}; // node_id -> {long_name, short_name} let paused = false; let activeFilter = 'all'; let pendingRows = []; // rows held while paused 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'); const tableWrapper = document.getElementById('pktTableWrapper'); // ── 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' }, }; const knownTypes = new Set(); // ── 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); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); return `${hh}:${mm}:${ss}`; } function fmtTo(toId) { if (toId === '4294967295' || toId === '^all' || toId === 'ffffffff') { return 'Alle'; } return `${nodeName(toId)}`; } function portnumBadge(portnum) { const cfg = PORTNUM_CFG[portnum]; if (cfg) { return `${cfg.label}`; } const short = portnum ? portnum.replace(/_APP$/, '') : '?'; return `${escapeHtml(short)}`; } 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.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 ''; const used = start - limit; return `${used}/${start}`; } // ── Filter bar ───────────────────────────────────────────── function renderFilterBar() { const types = ['all', ...Array.from(knownTypes).sort()]; pktFilterBar.innerHTML = types.map(t => { const label = t === 'all' ? 'Alle' : (PORTNUM_CFG[t]?.label || t.replace(/_APP$/, '')); const active = t === activeFilter; 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-portnum]').forEach(row => { const visible = activeFilter === 'all' || row.dataset.portnum === activeFilter; row.classList.toggle('d-none', !visible); }); } // ── Row rendering ────────────────────────────────────────── function buildRow(pkt) { const tr = document.createElement('tr'); tr.dataset.portnum = pkt.portnum || ''; if (activeFilter !== 'all' && tr.dataset.portnum !== activeFilter) { tr.classList.add('d-none'); } tr.innerHTML = `${fmtTime(pkt.timestamp)}` + `${nodeName(pkt.from_id)}` + `${fmtTo(pkt.to_id)}` + `${portnumBadge(pkt.portnum)}` + `${pkt.channel ?? '—'}` + `${fmtSnr(pkt.snr)}` + `${fmtRssi(pkt.rssi)}` + `${fmtHops(pkt.hop_limit, pkt.hop_start)}` + `${fmtPayload(pkt.portnum, pkt.payload)}`; return tr; } function addRow(pkt, prepend = true) { if (pkt.portnum) knownTypes.add(pkt.portnum); const row = buildRow(pkt); if (prepend) { pktBody.prepend(row); // Trim excess rows while (pktBody.children.length > MAX_ROWS) { pktBody.removeChild(pktBody.lastChild); } } else { pktBody.appendChild(row); } updateCount(); } function updateCount() { const total = pktBody.children.length; pktCount.textContent = `${total} Einträge`; } // ── Pause / Clear ────────────────────────────────────────── pktPauseBtn.addEventListener('click', () => { paused = !paused; const icon = pktPauseBtn.querySelector('i'); icon.className = paused ? 'bi bi-play-fill' : 'bi bi-pause-fill'; pktPauseBtn.title = paused ? 'Weiter' : 'Pause'; if (!paused && pendingRows.length) { pendingRows.forEach(p => addRow(p, true)); pendingRows = []; renderFilterBar(); } }); pktClearBtn.addEventListener('click', () => { pktBody.innerHTML = ''; pendingRows = []; updateCount(); }); // ── WebSocket ────────────────────────────────────────────── function connectWs() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; ws = new WebSocket(`${proto}://${location.host}/ws`); ws.onopen = () => { document.getElementById('statusDot').className = 'status-dot online'; document.getElementById('statusText').textContent = 'Verbunden'; }; ws.onclose = () => { document.getElementById('statusDot').className = 'status-dot'; document.getElementById('statusText').textContent = 'Getrennt'; setTimeout(connectWs, 3000); }; ws.onmessage = e => { const msg = JSON.parse(e.data); handleMsg(msg); }; } function handleMsg(msg) { switch (msg.type) { case 'initial': // Populate nodes map (msg.data || []).forEach(n => { if (n.node_id) nodes[n.node_id] = n; }); break; case 'node_update': if (msg.data && msg.data.node_id) nodes[msg.data.node_id] = msg.data; break; case 'initial_packets': // DB returns newest-first (DESC) — append in that order → newest at top pktBody.innerHTML = ''; (msg.data || []).forEach(p => { if (p.portnum) knownTypes.add(p.portnum); pktBody.appendChild(buildRow(p)); }); renderFilterBar(); updateCount(); break; case 'packet': if (paused) { pendingRows.push(msg.data); } else { if (msg.data.portnum) knownTypes.add(msg.data.portnum); addRow(msg.data, true); renderFilterBar(); } break; case 'bot_status': if (msg.data) { const dot = document.getElementById('statusDot'); const text = document.getElementById('statusText'); if (dot && text) { dot.className = 'status-dot ' + (msg.data.connected ? 'online' : 'offline'); text.textContent = msg.data.connected ? 'Verbunden' : 'Getrennt'; } } break; } } // ── Init ─────────────────────────────────────────────────── initPage(); connectWs();