diff --git a/public/packets.js b/public/packets.js index 37feab29..2a1a4f4b 100644 --- a/public/packets.js +++ b/public/packets.js @@ -184,6 +184,10 @@
+
+ + +
@@ -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 => + `
${escapeHtml(n.name || n.public_key.slice(0,8))} ${n.public_key.slice(0,8)}
` + ).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'); } diff --git a/public/style.css b/public/style.css index a5089bc8..cf503042 100644 --- a/public/style.css +++ b/public/style.css @@ -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)); } diff --git a/server.js b/server.js index 046e412f..c8310a6c 100644 --- a/server.js +++ b/server.js @@ -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) });