diff --git a/public/packets.js b/public/packets.js
index bb3f638b..512ae404 100644
--- a/public/packets.js
+++ b/public/packets.js
@@ -233,8 +233,8 @@
- | Region | Time | Hash | Size |
- Type | Observer | Path | Rpt | Details |
+ | Region | Time | Hash | Size |
+ Type | Observer | Path | Rpt | Details |
@@ -278,10 +278,14 @@
const fNode = document.getElementById('fNode');
const fNodeDrop = document.getElementById('fNodeDropdown');
fNode.value = filters.nodeName || '';
+ let nodeActiveIdx = -1;
fNode.addEventListener('input', debounce(async (e) => {
const q = e.target.value.trim();
+ nodeActiveIdx = -1;
+ fNode.setAttribute('aria-activedescendant', '');
if (!q) {
fNodeDrop.classList.add('hidden');
+ fNode.setAttribute('aria-expanded', 'false');
if (filters.node) { filters.node = undefined; filters.nodeName = undefined; loadPackets(); }
return;
}
@@ -289,23 +293,64 @@
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)}
`
+ if (nodes.length === 0) { fNodeDrop.classList.add('hidden'); fNode.setAttribute('aria-expanded', 'false'); return; }
+ fNodeDrop.innerHTML = nodes.map((n, i) =>
+ `${escapeHtml(n.name || n.public_key.slice(0,8))} ${n.public_key.slice(0,8)}
`
).join('');
fNodeDrop.classList.remove('hidden');
+ fNode.setAttribute('aria-expanded', 'true');
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();
+ selectNodeOption(opt);
});
});
} catch {}
}, 250));
- fNode.addEventListener('blur', () => { setTimeout(() => fNodeDrop.classList.add('hidden'), 200); });
+
+ function selectNodeOption(opt) {
+ filters.node = opt.dataset.key;
+ filters.nodeName = opt.dataset.name;
+ fNode.value = opt.dataset.name;
+ fNodeDrop.classList.add('hidden');
+ fNode.setAttribute('aria-expanded', 'false');
+ fNode.setAttribute('aria-activedescendant', '');
+ nodeActiveIdx = -1;
+ loadPackets();
+ }
+
+ fNode.addEventListener('keydown', (e) => {
+ const options = fNodeDrop.querySelectorAll('.node-filter-option');
+ if (!options.length || fNodeDrop.classList.contains('hidden')) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ nodeActiveIdx = Math.min(nodeActiveIdx + 1, options.length - 1);
+ updateNodeActive(options);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ nodeActiveIdx = Math.max(nodeActiveIdx - 1, 0);
+ updateNodeActive(options);
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (nodeActiveIdx >= 0 && options[nodeActiveIdx]) selectNodeOption(options[nodeActiveIdx]);
+ } else if (e.key === 'Escape') {
+ fNodeDrop.classList.add('hidden');
+ fNode.setAttribute('aria-expanded', 'false');
+ nodeActiveIdx = -1;
+ }
+ });
+
+ function updateNodeActive(options) {
+ options.forEach((o, i) => {
+ o.classList.toggle('node-filter-active', i === nodeActiveIdx);
+ o.setAttribute('aria-selected', i === nodeActiveIdx ? 'true' : 'false');
+ });
+ if (nodeActiveIdx >= 0 && options[nodeActiveIdx]) {
+ fNode.setAttribute('aria-activedescendant', options[nodeActiveIdx].id);
+ options[nodeActiveIdx].scrollIntoView({ block: 'nearest' });
+ }
+ }
+
+ fNode.addEventListener('blur', () => { setTimeout(() => { fNodeDrop.classList.add('hidden'); fNode.setAttribute('aria-expanded', 'false'); }, 200); });
// Delegated click/keyboard handler for table rows
const pktBody = document.getElementById('pktBody');
@@ -353,14 +398,14 @@
const isSingle = p.count <= 1;
html += `
| ${isSingle ? '' : (isExpanded ? '▼' : '▶')} |
- ${groupRegion ? `${groupRegion}` : '—'} |
+ ${groupRegion ? `${groupRegion}` : '—'} |
${timeAgo(p.latest)} |
${truncate(p.hash || '—', 8)} |
- ${groupSize ? groupSize + 'B' : '—'} |
+ ${groupSize ? groupSize + 'B' : '—'} |
${p.payload_type != null ? `${groupTypeName}` : '—'} |
${isSingle ? truncate(p.observer_name || p.observer_id || '—', 16) : truncate(p.observer_name || p.observer_id || '—', 10) + (p.observer_count > 1 ? ' +' + (p.observer_count - 1) : '')} |
${groupPathStr} |
- ${isSingle ? '' : p.count} |
+ ${isSingle ? '' : p.count} |
${getDetailPreview((() => { try { return JSON.parse(p.decoded_json || '{}'); } catch { return {}; } })())} |
`;
// Child rows (loaded async when expanded)
@@ -374,14 +419,14 @@
try { childPath = JSON.parse(c.path_json || '[]'); } catch {}
const childPathStr = renderPath(childPath);
html += `
- | ${childRegion ? `${childRegion}` : '—'} |
+ | ${childRegion ? `${childRegion}` : '—'} |
${timeAgo(c.timestamp)} |
${truncate(c.hash || '', 8)} |
- ${size}B |
+ ${size}B |
${typeName} |
${truncate(c.observer_name || c.observer_id || '—', 16)} |
${childPathStr} |
- |
+ |
${getDetailPreview((() => { try { return JSON.parse(c.decoded_json); } catch { return {}; } })())} |
`;
}
@@ -404,14 +449,14 @@
const detail = getDetailPreview(decoded);
return `
- | ${region ? `${region}` : '—'} |
+ | ${region ? `${region}` : '—'} |
${timeAgo(p.timestamp)} |
${truncate(p.hash || String(p.id), 8)} |
- ${size}B |
+ ${size}B |
${typeName} |
${truncate(p.observer_name || p.observer_id || '—', 16)} |
${pathStr} |
- |
+ |
${detail} |
`;
}).join('');
@@ -664,9 +709,10 @@
// BYOP modal — decode only, no DB injection
function showBYOP() {
+ const triggerBtn = document.querySelector('[data-action="pkt-byop"]');
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
- overlay.innerHTML = ''
+ overlay.innerHTML = '
'
+ ''
+ '
Paste raw hex bytes from your radio or MQTT feed:
'
+ '
'
@@ -675,10 +721,30 @@
+ '
';
document.body.appendChild(overlay);
- const close = () => overlay.remove();
+ const modal = overlay.querySelector('.byop-modal');
+ const close = () => { overlay.remove(); if (triggerBtn) triggerBtn.focus(); };
overlay.querySelector('.byop-x').onclick = close;
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
+ // Focus trap
+ function getFocusable() {
+ return modal.querySelectorAll('textarea, button, input, [tabindex]:not([tabindex="-1"])');
+ }
+ overlay.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') { e.preventDefault(); close(); return; }
+ if (e.key === 'Tab') {
+ const focusable = getFocusable();
+ if (!focusable.length) return;
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+ if (e.shiftKey) {
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
+ } else {
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
+ }
+ }
+ });
+
const textarea = overlay.querySelector('#byopHex');
textarea.focus();
textarea.addEventListener('keydown', (e) => {
diff --git a/public/style.css b/public/style.css
index c6247bbf..152670f4 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1115,6 +1115,12 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
transition: background 0.1s;
}
.node-filter-option:hover { background: var(--surface-2, rgba(255,255,255,0.08)); }
+.node-filter-option.node-filter-active { background: var(--accent); color: #fff; }
+
+/* Hide low-value columns on mobile */
+@media (max-width: 640px) {
+ .col-region, .col-rpt, .col-size { display: none; }
+}
/* Clickable hop links */
.hop-link {