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
|
# 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
|
## [0.08.24] - 2026-02-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.08.24"
|
version: "0.08.25"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ let paused = false;
|
||||||
let activeFilter = 'all';
|
let activeFilter = 'all';
|
||||||
let pendingRows = [];
|
let pendingRows = [];
|
||||||
|
|
||||||
|
let filterFrom = '';
|
||||||
|
let filterTo = '';
|
||||||
|
let filterChannel = '';
|
||||||
|
let filterHops = '';
|
||||||
|
let searchText = '';
|
||||||
|
|
||||||
const pktBody = document.getElementById('pktBody');
|
const pktBody = document.getElementById('pktBody');
|
||||||
const pktCount = document.getElementById('pktCount');
|
const pktCount = document.getElementById('pktCount');
|
||||||
const pktFilterBar = document.getElementById('pktFilterBar');
|
const pktFilterBar = document.getElementById('pktFilterBar');
|
||||||
|
|
@ -194,20 +200,74 @@ pktFilterBar.addEventListener('click', e => {
|
||||||
applyFilter();
|
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() {
|
function applyFilter() {
|
||||||
pktBody.querySelectorAll('tr[data-type]').forEach(row => {
|
pktBody.querySelectorAll('tr[data-type]').forEach(row => {
|
||||||
const visible = activeFilter === 'all' || row.dataset.type === activeFilter;
|
row.classList.toggle('d-none', !rowVisible(row));
|
||||||
row.classList.toggle('d-none', !visible);
|
|
||||||
});
|
});
|
||||||
|
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 ──────────────────────────────────────────────
|
// ── 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) {
|
function buildRow(pkt) {
|
||||||
const key = typeKey(pkt.portnum);
|
const key = typeKey(pkt.portnum);
|
||||||
|
const sd = buildSearchData(pkt, key);
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.dataset.type = key;
|
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 =
|
tr.innerHTML =
|
||||||
`<td class="text-body-secondary" style="font-size:.75rem;white-space:nowrap">${fmtTime(pkt.timestamp)}</td>` +
|
`<td class="text-body-secondary" style="font-size:.75rem;white-space:nowrap">${fmtTime(pkt.timestamp)}</td>` +
|
||||||
|
|
@ -285,6 +345,7 @@ function handleMsg(msg) {
|
||||||
break;
|
break;
|
||||||
case 'channels':
|
case 'channels':
|
||||||
channels = msg.data || {};
|
channels = msg.data || {};
|
||||||
|
fillChannelSelect();
|
||||||
break;
|
break;
|
||||||
case 'my_node_id':
|
case 'my_node_id':
|
||||||
myNodeId = msg.data;
|
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 ───────────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
initPage();
|
initPage();
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,22 @@
|
||||||
<i class="bi bi-trash" style="font-size:.75rem"></i>
|
<i class="bi bi-trash" style="font-size:.75rem"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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="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">
|
<table class="table table-sm table-hover mb-0" id="pktTable">
|
||||||
<thead class="sticky-top">
|
<thead class="sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue