diff --git a/CHANGELOG.md b/CHANGELOG.md index 82530d6..0300b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.08.25] - 2026-02-20 + +### Added +- **Paket-Log: Erweiterte Filterzeile** (closes #6): Unterhalb des Typ-Filters + neue Zeile mit Von, An, Kanal-Dropdown, Hops-Maximalwert und Freitextsuche. + Alle Filter wirken kombiniert (AND-Logik). Reset-Button (✕) setzt alle + Zusatzfilter zurück. `buildRow()` befüllt `data-from/to/channel/hops/search` + für performante DOM-Filterung ohne Re-Render. + ## [0.08.24] - 2026-02-20 ### Added diff --git a/config/config.yaml b/config/config.yaml index 507b0e8..9d93dfc 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,4 +1,4 @@ -version: "0.08.24" +version: "0.08.25" bot: name: "MeshDD-Bot" diff --git a/static/js/packets.js b/static/js/packets.js index 0d853de..bba9114 100644 --- a/static/js/packets.js +++ b/static/js/packets.js @@ -11,6 +11,12 @@ let paused = false; let activeFilter = 'all'; let pendingRows = []; +let filterFrom = ''; +let filterTo = ''; +let filterChannel = ''; +let filterHops = ''; +let searchText = ''; + const pktBody = document.getElementById('pktBody'); const pktCount = document.getElementById('pktCount'); const pktFilterBar = document.getElementById('pktFilterBar'); @@ -194,20 +200,74 @@ pktFilterBar.addEventListener('click', e => { applyFilter(); }); +function rowVisible(row) { + const typeOk = activeFilter === 'all' || row.dataset.type === activeFilter; + const fromOk = !filterFrom || (row.dataset.from || '').includes(filterFrom); + const toOk = !filterTo || (row.dataset.to || '').includes(filterTo); + const chOk = !filterChannel || row.dataset.channel === filterChannel; + const hopsOk = filterHops === '' || ( + row.dataset.hops !== '' && + parseInt(row.dataset.hops, 10) <= parseInt(filterHops, 10) + ); + const searchOk = !searchText || (row.dataset.search || '').includes(searchText); + return typeOk && fromOk && toOk && chOk && hopsOk && searchOk; +} + function applyFilter() { pktBody.querySelectorAll('tr[data-type]').forEach(row => { - const visible = activeFilter === 'all' || row.dataset.type === activeFilter; - row.classList.toggle('d-none', !visible); + row.classList.toggle('d-none', !rowVisible(row)); }); + const hasFilter = filterFrom || filterTo || filterChannel || filterHops !== '' || searchText; + document.getElementById('fClearBtn').classList.toggle('d-none', !hasFilter); +} + +function fillChannelSelect() { + const sel = document.getElementById('fChSelect'); + if (!sel) return; + const prev = sel.value; + while (sel.options.length > 1) sel.remove(1); + Object.entries(channels).sort(([a], [b]) => Number(a) - Number(b)).forEach(([idx, name]) => { + const opt = document.createElement('option'); + opt.value = String(idx); + opt.textContent = name ? `${idx}: ${name}` : String(idx); + sel.appendChild(opt); + }); + sel.value = prev; } // ── Row rendering ────────────────────────────────────────────── +function buildSearchData(pkt, key) { + const fromName = `${pkt.from_id || ''} ${nodes[pkt.from_id]?.long_name || ''} ${nodes[pkt.from_id]?.short_name || ''}`.toLowerCase().trim(); + const isBcast = !pkt.to_id || ['4294967295', 'ffffffff'].includes(String(pkt.to_id)); + const toName = isBcast + ? 'alle broadcast' + : `${pkt.to_id} ${nodes[pkt.to_id]?.long_name || ''} ${nodes[pkt.to_id]?.short_name || ''}`.toLowerCase().trim(); + const chName = `${pkt.channel ?? ''} ${channels[pkt.channel] || channels[Number(pkt.channel)] || ''}`.toLowerCase().trim(); + let payloadTxt = ''; + try { + const p = JSON.parse(pkt.payload || '{}'); + payloadTxt = Object.values(p).filter(v => typeof v === 'string' || typeof v === 'number').join(' '); + } catch {} + return { + from: fromName, + to: toName, + search: `${fromName} ${toName} ${typeCfg(key).label.toLowerCase()} ${chName} ${payloadTxt}`.toLowerCase(), + }; +} + 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'); + const key = typeKey(pkt.portnum); + const sd = buildSearchData(pkt, key); + const tr = document.createElement('tr'); + tr.dataset.type = key; + tr.dataset.from = sd.from; + tr.dataset.to = sd.to; + tr.dataset.channel = String(pkt.channel ?? ''); + tr.dataset.hops = (pkt.hop_start != null && pkt.hop_limit != null) + ? String(pkt.hop_start - pkt.hop_limit) : ''; + tr.dataset.search = sd.search; + if (!rowVisible(tr)) tr.classList.add('d-none'); tr.innerHTML = `