MeshDD-Bot/static/js/packets.js
ppfeiffer cc392a125a fix(packets): informativer Payload + eigene Telemetrie unterdrücken (fixes #3)
- bot.py: vollständige Payload-Daten für Position, Telemetry (Device+Env),
  NodeInfo (hw_model), Routing (error), Traceroute (hops), Neighborinfo (count)
- packets.js: fmtPayload() zeigt alle Felder; TELEMETRY_APP vom eigenen Node
  (my_node_id / short_name FTLW) wird unterdrückt und nicht gezählt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 17:20:35 +01:00

325 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 myNodeId = null;
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 '<span class="text-body-secondary">Alle</span>';
}
return `<span ${nodeTitle(toId)}>${nodeName(toId)}</span>`;
}
function fmtChannel(ch) {
if (ch == null) return '<span class="text-body-secondary">—</span>';
const name = channels[ch];
if (name) return `<span title="Ch ${ch}">${escapeHtml(name)}</span>`;
return `<span>${ch}</span>`;
}
// Colors that need dark text on their background
const _LIGHT_BG = new Set(['warning', 'yellow', 'lime']);
function portnumBadge(portnum) {
const key = typeKey(portnum);
const cfg = typeCfg(key);
const txtCls = _LIGHT_BG.has(cfg.color) ? 'text-dark' : 'text-white';
return `<span class="badge bg-${cfg.color} pkt-type-badge ${txtCls}">${escapeHtml(cfg.label)}</span>`;
}
function isSuppressed(pkt) {
if (pkt.portnum !== 'TELEMETRY_APP') return false;
if (myNodeId && pkt.from_id === myNodeId) return true;
const n = nodes[pkt.from_id];
return !!(n && n.short_name === 'FTLW');
}
function fmtPayload(portnum, payloadStr) {
let p = {};
try { p = JSON.parse(payloadStr || '{}'); } catch { return ''; }
if (portnum === 'TEXT_MESSAGE_APP' && p.text)
return `<span class="text-body">${escapeHtml(p.text)}</span>`;
if (portnum === 'POSITION_APP' && p.lat != null) {
const parts = [`${p.lat.toFixed(5)}, ${p.lon.toFixed(5)}`];
if (p.alt != null && p.alt !== 0) parts.push(`${p.alt} m`);
if (p.speed != null && p.speed !== 0) parts.push(`${p.speed} km/h`);
if (p.sats != null) parts.push(`${p.sats} Sat`);
return `<span class="text-body-secondary">${parts.join(' · ')}</span>`;
}
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`);
if (p.ch_util != null) parts.push(`CH ${p.ch_util.toFixed(1)}%`);
if (p.air_util != null) parts.push(`TX ${p.air_util.toFixed(1)}%`);
if (p.temp != null) parts.push(`🌡 ${p.temp.toFixed(1)} °C`);
if (p.humidity != null) parts.push(`💧 ${p.humidity.toFixed(0)}%`);
if (p.pressure != null) parts.push(`${p.pressure.toFixed(0)} hPa`);
return parts.length ? `<span class="text-body-secondary">${parts.join(' · ')}</span>` : '';
}
if (portnum === 'NODEINFO_APP' && (p.long_name || p.short_name)) {
let s = escapeHtml(p.long_name || '');
if (p.short_name) s += ` [${escapeHtml(p.short_name)}]`;
if (p.hw_model) s += ` <span class="opacity-50">${escapeHtml(p.hw_model)}</span>`;
return `<span class="text-body-secondary">${s}</span>`;
}
if (portnum === 'ROUTING_APP' && p.error && p.error !== 'NONE')
return `<span class="text-danger">${escapeHtml(p.error)}</span>`;
if (portnum === 'TRACEROUTE_APP' && p.hops != null)
return `<span class="text-body-secondary">${p.hops} Hop${p.hops !== 1 ? 's' : ''}</span>`;
if (portnum === 'NEIGHBORINFO_APP' && p.count != null)
return `<span class="text-body-secondary">${p.count} Nachbar${p.count !== 1 ? 'n' : ''}</span>`;
return '';
}
function fmtSnr(v) {
if (v == null) return '<span class="text-body-secondary">—</span>';
const cls = v >= 5 ? 'text-success' : v >= 0 ? 'text-warning' : 'text-danger';
return `<span class="${cls}">${v > 0 ? '+' : ''}${v}</span>`;
}
function fmtRssi(v) {
if (v == null) return '<span class="text-body-secondary">—</span>';
const cls = v >= -100 ? 'text-success' : v >= -115 ? 'text-warning' : 'text-danger';
return `<span class="${cls}">${v}</span>`;
}
function fmtHops(limit, start) {
if (start == null || limit == null) return '<span class="text-body-secondary">—</span>';
return `<span class="text-body-secondary">${start - limit}/${start}</span>`;
}
// ── 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 `<button class="btn btn-sm py-0 px-2 pkt-filter-btn ${active ? 'btn-secondary' : 'btn-outline-secondary'}"
data-type="all" style="font-size:.7rem">Alle</button>`;
}
const cfg = typeCfg(t);
return `<button class="btn btn-sm py-0 px-2 pkt-filter-btn ${active ? 'btn-' + cfg.color : 'btn-outline-' + cfg.color}"
data-type="${escapeHtml(t)}" style="font-size:.7rem">${escapeHtml(cfg.label)}</button>`;
}).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 =
`<td class="text-body-secondary" style="font-size:.75rem;white-space:nowrap">${fmtTime(pkt.timestamp)}</td>` +
`<td style="font-size:.8rem;white-space:nowrap"><span ${nodeTitle(pkt.from_id)}>${nodeName(pkt.from_id)}</span></td>` +
`<td style="font-size:.8rem;white-space:nowrap">${fmtTo(pkt.to_id)}</td>` +
`<td>${portnumBadge(pkt.portnum)}</td>` +
`<td style="font-size:.8rem;white-space:nowrap">${fmtChannel(pkt.channel)}</td>` +
`<td style="font-size:.8rem">${fmtSnr(pkt.snr)}</td>` +
`<td style="font-size:.8rem">${fmtRssi(pkt.rssi)}</td>` +
`<td style="font-size:.8rem">${fmtHops(pkt.hop_limit, pkt.hop_start)}</td>` +
`<td style="font-size:.8rem">${fmtPayload(pkt.portnum, pkt.payload)}</td>`;
return tr;
}
function addRow(pkt, prepend = true) {
if (isSuppressed(pkt)) return;
knownTypes.add(typeKey(pkt.portnum));
const row = buildRow(pkt);
if (prepend) {
pktBody.prepend(row);
while (pktBody.children.length > MAX_ROWS) pktBody.removeChild(pktBody.lastChild);
} else {
pktBody.appendChild(row);
}
updateCount();
}
function updateCount() {
pktCount.textContent = `${pktBody.children.length} Einträge`;
}
// ── Pause / Clear ──────────────────────────────────────────────
pktPauseBtn.addEventListener('click', () => {
paused = !paused;
pktPauseBtn.querySelector('i').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').classList.add('connected');
document.getElementById('statusText').textContent = 'Verbunden';
};
ws.onclose = () => {
document.getElementById('statusDot').classList.remove('connected');
document.getElementById('statusText').textContent = 'Getrennt';
setTimeout(connectWs, 3000);
};
ws.onmessage = e => handleMsg(JSON.parse(e.data));
}
function handleMsg(msg) {
switch (msg.type) {
case 'initial':
(msg.data || []).forEach(n => { if (n.node_id) nodes[n.node_id] = n; });
break;
case 'node_update':
if (msg.data?.node_id) nodes[msg.data.node_id] = msg.data;
break;
case 'channels':
channels = msg.data || {};
break;
case 'my_node_id':
myNodeId = msg.data;
break;
case 'initial_packets':
pktBody.innerHTML = '';
(msg.data || []).forEach(p => {
if (isSuppressed(p)) return;
knownTypes.add(typeKey(p.portnum));
pktBody.appendChild(buildRow(p));
});
renderFilterBar();
updateCount();
break;
case 'packet':
if (paused) {
pendingRows.push(msg.data);
} else {
addRow(msg.data, true);
renderFilterBar();
}
break;
case 'bot_status':
if (msg.data) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
if (dot) dot.classList.toggle('connected', !!msg.data.connected);
if (text) text.textContent = msg.data.connected ? 'Verbunden' : 'Getrennt';
}
break;
}
}
// ── Init ───────────────────────────────────────────────────────
initPage();
connectWs();