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) });