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 = `