diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe021c..7409800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.6.11] - 2026-02-18 + +### Changed +- **Paket-Log**: Badges gleich breit (CSS `min-width:5.5rem`, zentriert), Typ-Filter-Pills in + Typ-Farbe eingefärbt (aktiv: gefüllt, inaktiv: Outline), unbekannte/undekodierbare Pakete + (leerer Portnum) als Typ „?" im Filter sichtbar, Kanal-Spalte zeigt Kanalname wenn verfügbar. + ## [0.6.10] - 2026-02-18 ### Added diff --git a/config.yaml b/config.yaml index 03afb09..b51f016 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.6.10" +version: "0.6.11" bot: name: "MeshDD-Bot" diff --git a/static/css/style.css b/static/css/style.css index 710011d..81de6f9 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -279,6 +279,18 @@ flex-shrink: 0; } +/* ── Packet-Log Badges ───────────────────────────────────────── */ + +.pkt-type-badge { + display: inline-block; + min-width: 5.5rem; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: .65rem !important; +} + /* ── Scrollbars ──────────────────────────────────────────────── */ .table-responsive::-webkit-scrollbar, diff --git a/static/js/packets.js b/static/js/packets.js index 8c239f6..3c771b0 100644 --- a/static/js/packets.js +++ b/static/js/packets.js @@ -1,37 +1,47 @@ // MeshDD-Bot – Paket-Log const MAX_ROWS = 300; +const UNKNOWN_TYPE = '__unknown__'; let ws = null; -let nodes = {}; // node_id -> {long_name, short_name} -let paused = false; +let nodes = {}; // node_id -> {long_name, short_name, ...} +let channels = {}; // ch_index -> channel name string +let paused = false; let activeFilter = 'all'; -let pendingRows = []; // rows held while paused +let pendingRows = []; -const pktBody = document.getElementById('pktBody'); -const pktCount = document.getElementById('pktCount'); +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 ───────────────────────────────────────── +// ── 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' }, + 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(); -// ── Helpers ──────────────────────────────────────────────── +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 '—'; @@ -55,46 +65,45 @@ function nodeTitle(id) { 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}`; + return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; } function fmtTo(toId) { - if (toId === '4294967295' || toId === '^all' || toId === 'ffffffff') { + 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 cfg = PORTNUM_CFG[portnum]; - if (cfg) { - return `${cfg.label}`; - } - const short = portnum ? portnum.replace(/_APP$/, '') : '?'; - return `${escapeHtml(short)}`; + 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) { + if (portnum === 'TEXT_MESSAGE_APP' && p.text) return `${escapeHtml(p.text)}`; - } - if (portnum === 'POSITION_APP' && p.lat != null) { + 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(' · ')}`; + return parts.length ? `${parts.join(' · ')}` : ''; } - if (portnum === 'NODEINFO_APP' && (p.long_name || p.short_name)) { + if (portnum === 'NODEINFO_APP' && (p.long_name || p.short_name)) return `${escapeHtml(p.long_name || '')}${p.short_name ? ` [${escapeHtml(p.short_name)}]` : ''}`; - } return ''; } @@ -112,19 +121,28 @@ function fmtRssi(v) { function fmtHops(limit, start) { if (start == null || limit == null) return ''; - const used = start - limit; - return `${used}/${start}`; + return `${start - limit}/${start}`; } -// ── Filter bar ───────────────────────────────────────────── +// ── Filter bar ───────────────────────────────────────────────── function renderFilterBar() { - const types = ['all', ...Array.from(knownTypes).sort()]; + 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 label = t === 'all' ? 'Alle' : (PORTNUM_CFG[t]?.label || t.replace(/_APP$/, '')); const active = t === activeFilter; - return ``; + if (t === 'all') { + return ``; + } + const cfg = typeCfg(t); + return ``; }).join(''); } @@ -137,26 +155,26 @@ pktFilterBar.addEventListener('click', e => { }); function applyFilter() { - pktBody.querySelectorAll('tr[data-portnum]').forEach(row => { - const visible = activeFilter === 'all' || row.dataset.portnum === activeFilter; + pktBody.querySelectorAll('tr[data-type]').forEach(row => { + const visible = activeFilter === 'all' || row.dataset.type === activeFilter; row.classList.toggle('d-none', !visible); }); } -// ── Row rendering ────────────────────────────────────────── +// ── 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'); - } + 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 = `${fmtTime(pkt.timestamp)}` + `${nodeName(pkt.from_id)}` + `${fmtTo(pkt.to_id)}` + `${portnumBadge(pkt.portnum)}` + - `${pkt.channel ?? '—'}` + + `${fmtChannel(pkt.channel)}` + `${fmtSnr(pkt.snr)}` + `${fmtRssi(pkt.rssi)}` + `${fmtHops(pkt.hop_limit, pkt.hop_start)}` + @@ -165,14 +183,11 @@ function buildRow(pkt) { } function addRow(pkt, prepend = true) { - if (pkt.portnum) knownTypes.add(pkt.portnum); + knownTypes.add(typeKey(pkt.portnum)); const row = buildRow(pkt); if (prepend) { pktBody.prepend(row); - // Trim excess rows - while (pktBody.children.length > MAX_ROWS) { - pktBody.removeChild(pktBody.lastChild); - } + while (pktBody.children.length > MAX_ROWS) pktBody.removeChild(pktBody.lastChild); } else { pktBody.appendChild(row); } @@ -180,16 +195,14 @@ function addRow(pkt, prepend = true) { } function updateCount() { - const total = pktBody.children.length; - pktCount.textContent = `${total} Einträge`; + pktCount.textContent = `${pktBody.children.length} Einträge`; } -// ── Pause / Clear ────────────────────────────────────────── +// ── Pause / Clear ────────────────────────────────────────────── pktPauseBtn.addEventListener('click', () => { paused = !paused; - const icon = pktPauseBtn.querySelector('i'); - icon.className = paused ? 'bi bi-play-fill' : 'bi bi-pause-fill'; + 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)); @@ -204,43 +217,38 @@ pktClearBtn.addEventListener('click', () => { updateCount(); }); -// ── WebSocket ────────────────────────────────────────────── +// ── 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'; + ws.onopen = () => { + document.getElementById('statusDot').classList.add('connected'); document.getElementById('statusText').textContent = 'Verbunden'; }; ws.onclose = () => { - document.getElementById('statusDot').className = 'status-dot'; + document.getElementById('statusDot').classList.remove('connected'); document.getElementById('statusText').textContent = 'Getrennt'; setTimeout(connectWs, 3000); }; - ws.onmessage = e => { - const msg = JSON.parse(e.data); - handleMsg(msg); - }; + ws.onmessage = e => handleMsg(JSON.parse(e.data)); } function handleMsg(msg) { switch (msg.type) { case 'initial': - // Populate nodes map - (msg.data || []).forEach(n => { - if (n.node_id) nodes[n.node_id] = n; - }); + (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; + if (msg.data?.node_id) nodes[msg.data.node_id] = msg.data; + break; + case 'channels': + channels = 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); + knownTypes.add(typeKey(p.portnum)); pktBody.appendChild(buildRow(p)); }); renderFilterBar(); @@ -250,7 +258,6 @@ function handleMsg(msg) { if (paused) { pendingRows.push(msg.data); } else { - if (msg.data.portnum) knownTypes.add(msg.data.portnum); addRow(msg.data, true); renderFilterBar(); } @@ -259,16 +266,14 @@ function handleMsg(msg) { 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'; - } + if (dot) dot.classList.toggle('connected', !!msg.data.connected); + if (text) text.textContent = msg.data.connected ? 'Verbunden' : 'Getrennt'; } break; } } -// ── Init ─────────────────────────────────────────────────── +// ── Init ─────────────────────────────────────────────────────── initPage(); connectWs();