mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-25 12:44:01 +00:00
Add node name filter to packets page, fix duplicate node WHERE clause
- Autocomplete dropdown searches /api/nodes/search as you type - Selecting a node filters packets by that node's pubkey - Fixed duplicate node filter condition in grouped packets query
This commit is contained in:
@@ -184,6 +184,10 @@
|
||||
</div>
|
||||
<div class="filter-bar" id="pktFilters">
|
||||
<input type="text" placeholder="Packet hash…" id="fHash">
|
||||
<div class="node-filter-wrap" style="position:relative">
|
||||
<input type="text" placeholder="Node name…" id="fNode" autocomplete="off">
|
||||
<div class="node-filter-dropdown hidden" id="fNodeDropdown"></div>
|
||||
</div>
|
||||
<select id="fObserver"><option value="">All Observers</option></select>
|
||||
<select id="fRegion"><option value="">All Regions</option></select>
|
||||
<select id="fType"><option value="">All Types</option></select>
|
||||
@@ -222,6 +226,39 @@
|
||||
document.getElementById('fType').addEventListener('change', (e) => { filters.type = e.target.value !== '' ? e.target.value : undefined; loadPackets(); });
|
||||
document.getElementById('fGroup').addEventListener('click', () => { groupByHash = !groupByHash; loadPackets(); });
|
||||
|
||||
// Node name filter with autocomplete
|
||||
const fNode = document.getElementById('fNode');
|
||||
const fNodeDrop = document.getElementById('fNodeDropdown');
|
||||
fNode.value = filters.nodeName || '';
|
||||
fNode.addEventListener('input', debounce(async (e) => {
|
||||
const q = e.target.value.trim();
|
||||
if (!q) {
|
||||
fNodeDrop.classList.add('hidden');
|
||||
if (filters.node) { filters.node = undefined; filters.nodeName = undefined; loadPackets(); }
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/nodes/search?q=' + encodeURIComponent(q));
|
||||
const data = await resp.json();
|
||||
const nodes = data.nodes || [];
|
||||
if (nodes.length === 0) { fNodeDrop.classList.add('hidden'); return; }
|
||||
fNodeDrop.innerHTML = nodes.map(n =>
|
||||
`<div class="node-filter-option" data-key="${n.public_key}" data-name="${escapeHtml(n.name || n.public_key.slice(0,8))}">${escapeHtml(n.name || n.public_key.slice(0,8))} <span style="color:var(--muted);font-size:0.8em">${n.public_key.slice(0,8)}</span></div>`
|
||||
).join('');
|
||||
fNodeDrop.classList.remove('hidden');
|
||||
fNodeDrop.querySelectorAll('.node-filter-option').forEach(opt => {
|
||||
opt.addEventListener('click', () => {
|
||||
filters.node = opt.dataset.key;
|
||||
filters.nodeName = opt.dataset.name;
|
||||
fNode.value = opt.dataset.name;
|
||||
fNodeDrop.classList.add('hidden');
|
||||
loadPackets();
|
||||
});
|
||||
});
|
||||
} catch {}
|
||||
}, 250));
|
||||
fNode.addEventListener('blur', () => { setTimeout(() => fNodeDrop.classList.add('hidden'), 200); });
|
||||
|
||||
renderTableRows();
|
||||
makeColumnsResizable('#pktTable', 'meshcore-pkt-col-widths');
|
||||
}
|
||||
|
||||
@@ -1084,3 +1084,27 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.replay-live-btn:hover { background: rgba(168, 85, 247, 0.3); }
|
||||
|
||||
/* Node filter dropdown */
|
||||
.node-filter-wrap { display: inline-block; }
|
||||
.node-filter-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface-1, #1e293b);
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
||||
border-radius: 6px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
.node-filter-dropdown.hidden { display: none; }
|
||||
.node-filter-option {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.node-filter-option:hover { background: var(--surface-2, rgba(255,255,255,0.08)); }
|
||||
|
||||
@@ -288,7 +288,6 @@ app.get('/api/packets', (req, res) => {
|
||||
if (observer) { where.push('observer_id = @observer'); params.observer = observer; }
|
||||
if (hash) { where.push('hash = @hash'); params.hash = hash; }
|
||||
if (since) { where.push('timestamp > @since'); params.since = since; }
|
||||
if (node) { where.push("(decoded_json LIKE @nodePattern OR decoded_json LIKE @nodeNamePattern)"); if (!params.nodePattern) { params.nodePattern = `%${node}%`; const n = db.db.prepare('SELECT name FROM nodes WHERE public_key = ?').get(node); params.nodeNamePattern = n ? `%${n.name}%` : `%${node}%`; } }
|
||||
if (node) { where.push("(decoded_json LIKE @nodePattern OR decoded_json LIKE @nodeNamePattern)"); params.nodePattern = `%${node}%`; const n = db.db.prepare('SELECT name FROM nodes WHERE public_key = ?').get(node); params.nodeNamePattern = n ? `%${n.name}%` : `%${node}%`; }
|
||||
const clause = where.length ? 'WHERE ' + where.join(' AND ') : '';
|
||||
const packets = db.db.prepare(`SELECT hash, COUNT(DISTINCT observer_id) as observer_count, COUNT(*) as count, MAX(timestamp) as latest, (SELECT observer_id FROM packets pObs WHERE pObs.hash = packets.hash ORDER BY pObs.timestamp ASC LIMIT 1) as observer_id, (SELECT observer_name FROM packets pOn WHERE pOn.hash = packets.hash ORDER BY pOn.timestamp ASC LIMIT 1) as observer_name, (SELECT path_json FROM packets p2 WHERE p2.hash = packets.hash ORDER BY p2.timestamp DESC LIMIT 1) as path_json, (SELECT payload_type FROM packets p3 WHERE p3.hash = packets.hash ORDER BY p3.timestamp DESC LIMIT 1) as payload_type, (SELECT raw_hex FROM packets p4 WHERE p4.hash = packets.hash ORDER BY p4.timestamp DESC LIMIT 1) as raw_hex, (SELECT decoded_json FROM packets p5 WHERE p5.hash = packets.hash ORDER BY p5.timestamp DESC LIMIT 1) as decoded_json FROM packets ${clause} GROUP BY hash ORDER BY latest DESC LIMIT @limit OFFSET @offset`).all({ ...params, limit: Number(limit), offset: Number(offset) });
|
||||
|
||||
Reference in New Issue
Block a user