feat(packets): Erweiterte Filterzeile + Freitextsuche (closes #6)
- Filterzeile mit Von, An, Kanal-Dropdown, Hops (≤) und Freitextsuche - buildRow() befüllt data-from/to/channel/hops/search für performante Filterung - rowVisible() prüft alle aktiven Filter (AND-Logik) - Channel-Dropdown wird beim channels-WS-Event befüllt - Reset-Button setzt alle Zusatzfilter zurück Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
511ff20842
commit
f608f513a8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.08.24"
|
||||
version: "0.08.25"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -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 sd = buildSearchData(pkt, key);
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.type = key;
|
||||
if (activeFilter !== 'all' && key !== activeFilter) tr.classList.add('d-none');
|
||||
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 =
|
||||
`<td class="text-body-secondary" style="font-size:.75rem;white-space:nowrap">${fmtTime(pkt.timestamp)}</td>` +
|
||||
|
|
@ -285,6 +345,7 @@ function handleMsg(msg) {
|
|||
break;
|
||||
case 'channels':
|
||||
channels = msg.data || {};
|
||||
fillChannelSelect();
|
||||
break;
|
||||
case 'my_node_id':
|
||||
myNodeId = msg.data;
|
||||
|
|
@ -318,6 +379,44 @@ function handleMsg(msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Erweiterte Filter-Listener ──────────────────────────────────
|
||||
|
||||
document.getElementById('fVon').addEventListener('input', e => {
|
||||
filterFrom = e.target.value.trim().toLowerCase();
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
document.getElementById('fAn').addEventListener('input', e => {
|
||||
filterTo = e.target.value.trim().toLowerCase();
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
document.getElementById('fChSelect').addEventListener('change', e => {
|
||||
filterChannel = e.target.value;
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
document.getElementById('fHops').addEventListener('input', e => {
|
||||
filterHops = e.target.value.trim();
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
document.getElementById('fText').addEventListener('input', e => {
|
||||
searchText = e.target.value.trim().toLowerCase();
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
document.getElementById('fClearBtn').addEventListener('click', () => {
|
||||
filterFrom = filterTo = filterChannel = searchText = '';
|
||||
filterHops = '';
|
||||
document.getElementById('fVon').value = '';
|
||||
document.getElementById('fAn').value = '';
|
||||
document.getElementById('fChSelect').value = '';
|
||||
document.getElementById('fHops').value = '';
|
||||
document.getElementById('fText').value = '';
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────
|
||||
|
||||
initPage();
|
||||
|
|
|
|||
|
|
@ -55,8 +55,22 @@
|
|||
<i class="bi bi-trash" style="font-size:.75rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Erweiterte Filterzeile -->
|
||||
<div class="border-bottom px-2 py-1 d-flex gap-1 flex-wrap align-items-center" id="pktAdvFilters">
|
||||
<input type="text" class="form-control form-control-sm" id="fVon" placeholder="Von…" style="width:110px;font-size:.73rem">
|
||||
<input type="text" class="form-control form-control-sm" id="fAn" placeholder="An…" style="width:110px;font-size:.73rem">
|
||||
<select class="form-select form-select-sm" id="fChSelect" style="width:120px;font-size:.73rem">
|
||||
<option value="">Kanal: Alle</option>
|
||||
</select>
|
||||
<input type="number" class="form-control form-control-sm" id="fHops" placeholder="Hops ≤" style="width:75px;font-size:.73rem" min="0" max="9">
|
||||
<input type="search" class="form-control form-control-sm flex-grow-1" id="fText" placeholder="🔍 Freitext…" style="min-width:120px;font-size:.73rem">
|
||||
<button class="btn btn-sm btn-outline-secondary py-0 px-2 d-none" id="fClearBtn" title="Filter zurücksetzen">
|
||||
<i class="bi bi-x-lg" style="font-size:.7rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height:calc(100vh - 130px);overflow-y:auto" id="pktTableWrapper">
|
||||
<div class="table-responsive" style="max-height:calc(100vh - 168px);overflow-y:auto" id="pktTableWrapper">
|
||||
<table class="table table-sm table-hover mb-0" id="pktTable">
|
||||
<thead class="sticky-top">
|
||||
<tr>
|
||||
|
|
|
|||
Loading…
Reference in a new issue