mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-25 16:14:02 +00:00
## Summary
Fixes BYOP modal stacking on the Packets page by preventing duplicate
global click handlers and enforcing a single BYOP overlay instance.
## Root cause
Packets page init could register document-level click handlers
repeatedly across SPA navigations. Clicking BYOP then spawned multiple
overlays, and each close action removed only one layer.
## Changes
- `public/packets.js`
- Added `bindDocumentHandler(...)` to de-duplicate document click
handlers.
- Applied it to packets action delegation, filter menu outside-click
close, and column menu close.
- Added `removeAllByopOverlays()` and call it before opening BYOP.
- Tagged BYOP overlay with `.byop-overlay` class.
- Updated close logic to remove all BYOP overlays in one click.
- Scoped BYOP result lookup to the active overlay
(`overlay.querySelector`).
- Added destroy cleanup for document handlers and stray BYOP overlays.
- `test-frontend-helpers.js`
- Added regression tests for:
- BYOP singleton overlay behavior
- one-click close removing all overlays
- document click handler de-dup logic
- `public/index.html`
- Bumped cache busters for JS/CSS assets.
## Validation
- `node test-frontend-helpers.js`
- `node test-packet-filter.js`
- `node test-aging.js`
All passed locally.
Fixes #249
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+28
-28
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="CoreScope">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774920209">
|
||||
<link rel="stylesheet" href="home.css?v=1774920209">
|
||||
<link rel="stylesheet" href="live.css?v=1774920209">
|
||||
<link rel="stylesheet" href="style.css?v=1774920715">
|
||||
<link rel="stylesheet" href="home.css?v=1774920715">
|
||||
<link rel="stylesheet" href="live.css?v=1774920715">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,30 +81,30 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774920209"></script>
|
||||
<script src="customize.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774920209"></script>
|
||||
<script src="hop-resolver.js?v=1774920209"></script>
|
||||
<script src="hop-display.js?v=1774920209"></script>
|
||||
<script src="app.js?v=1774920209"></script>
|
||||
<script src="home.js?v=1774920209"></script>
|
||||
<script src="packet-filter.js?v=1774920209"></script>
|
||||
<script src="packets.js?v=1774920209"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774920209"></script>
|
||||
<script src="map.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774920715"></script>
|
||||
<script src="customize.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774920715"></script>
|
||||
<script src="hop-resolver.js?v=1774920715"></script>
|
||||
<script src="hop-display.js?v=1774920715"></script>
|
||||
<script src="app.js?v=1774920715"></script>
|
||||
<script src="home.js?v=1774920715"></script>
|
||||
<script src="packet-filter.js?v=1774920715"></script>
|
||||
<script src="packets.js?v=1774920715"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774920715"></script>
|
||||
<script src="map.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774920715" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+31
-6
@@ -157,9 +157,29 @@
|
||||
let directPacketId = null;
|
||||
let directPacketHash = null;
|
||||
let initGeneration = 0;
|
||||
let _docActionHandler = null;
|
||||
let _docMenuCloseHandler = null;
|
||||
let _docColMenuCloseHandler = null;
|
||||
|
||||
let directObsId = null;
|
||||
|
||||
function removeAllByopOverlays() {
|
||||
document.querySelectorAll('.byop-overlay').forEach(function (el) { el.remove(); });
|
||||
}
|
||||
|
||||
function bindDocumentHandler(kind, eventName, handler) {
|
||||
const prev = kind === 'action'
|
||||
? _docActionHandler
|
||||
: kind === 'menu'
|
||||
? _docMenuCloseHandler
|
||||
: _docColMenuCloseHandler;
|
||||
if (prev) document.removeEventListener(eventName, prev);
|
||||
document.addEventListener(eventName, handler);
|
||||
if (kind === 'action') _docActionHandler = handler;
|
||||
else if (kind === 'menu') _docMenuCloseHandler = handler;
|
||||
else _docColMenuCloseHandler = handler;
|
||||
}
|
||||
|
||||
function renderTimestampCell(isoString) {
|
||||
if (typeof formatTimestampWithTooltip !== 'function' || typeof getTimestampMode !== 'function') {
|
||||
return escapeHtml(typeof timeAgo === 'function' ? timeAgo(isoString) : '—');
|
||||
@@ -237,7 +257,7 @@
|
||||
}
|
||||
|
||||
// Event delegation for data-action buttons
|
||||
document.addEventListener('click', function (e) {
|
||||
bindDocumentHandler('action', 'click', function (e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
if (btn.dataset.action === 'pkt-refresh') loadPackets();
|
||||
@@ -376,6 +396,10 @@
|
||||
function destroy() {
|
||||
if (wsHandler) offWS(wsHandler);
|
||||
wsHandler = null;
|
||||
if (_docActionHandler) { document.removeEventListener('click', _docActionHandler); _docActionHandler = null; }
|
||||
if (_docMenuCloseHandler) { document.removeEventListener('click', _docMenuCloseHandler); _docMenuCloseHandler = null; }
|
||||
if (_docColMenuCloseHandler) { document.removeEventListener('click', _docColMenuCloseHandler); _docColMenuCloseHandler = null; }
|
||||
removeAllByopOverlays();
|
||||
packets = [];
|
||||
hashIndex = new Map(); selectedId = null;
|
||||
filtersBuilt = false;
|
||||
@@ -705,7 +729,7 @@
|
||||
});
|
||||
|
||||
// Close multi-select menus on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
bindDocumentHandler('menu', 'click', (e) => {
|
||||
const obsWrap = document.getElementById('observerFilterWrap');
|
||||
const typeWrap = document.getElementById('typeFilterWrap');
|
||||
if (obsWrap && !obsWrap.contains(e.target)) { const m = obsWrap.querySelector('.multi-select-menu'); if (m) m.classList.remove('open'); }
|
||||
@@ -820,7 +844,7 @@
|
||||
e.stopPropagation();
|
||||
colMenu.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('click', () => colMenu.classList.remove('open'));
|
||||
bindDocumentHandler('colmenu', 'click', () => colMenu.classList.remove('open'));
|
||||
applyColVisibility();
|
||||
|
||||
document.getElementById('hexHashToggle').addEventListener('click', function () {
|
||||
@@ -1548,9 +1572,10 @@
|
||||
|
||||
// BYOP modal — decode only, no DB injection
|
||||
function showBYOP() {
|
||||
removeAllByopOverlays();
|
||||
const triggerBtn = document.querySelector('[data-action="pkt-byop"]');
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.className = 'modal-overlay byop-overlay';
|
||||
overlay.innerHTML = '<div class="modal byop-modal" role="dialog" aria-label="Decode a Packet" aria-modal="true">'
|
||||
+ '<div class="byop-header"><h3>📦 Decode a Packet</h3><button class="btn-icon byop-x" title="Close" aria-label="Close dialog">✕</button></div>'
|
||||
+ '<p class="text-muted" style="margin:0 0 12px;font-size:.85rem">Paste raw hex bytes from your radio or MQTT feed:</p>'
|
||||
@@ -1561,7 +1586,7 @@
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const modal = overlay.querySelector('.byop-modal');
|
||||
const close = () => { overlay.remove(); if (triggerBtn) triggerBtn.focus(); };
|
||||
const close = () => { removeAllByopOverlays(); if (triggerBtn) triggerBtn.focus(); };
|
||||
overlay.querySelector('.byop-x').onclick = close;
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
@@ -1597,7 +1622,7 @@
|
||||
|
||||
async function doDecode() {
|
||||
const hex = textarea.value.trim().replace(/[\s\n]/g, '');
|
||||
const result = document.getElementById('byopResult');
|
||||
const result = overlay.querySelector('#byopResult');
|
||||
if (!hex) { result.innerHTML = '<p class="text-muted">Enter hex data</p>'; return; }
|
||||
if (!/^[0-9a-fA-F]+$/.test(hex)) { result.innerHTML = '<p class="byop-err" role="alert">Invalid hex — only 0-9 and A-F allowed</p>'; return; }
|
||||
result.innerHTML = '<p class="text-muted">Decoding...</p>';
|
||||
|
||||
@@ -1318,6 +1318,29 @@ console.log('\n=== compare.js: comparePacketSets ===');
|
||||
assert.ok(packetsSource.includes("classList.remove('detail-collapsed')"),
|
||||
'selectPacket should remove detail-collapsed class');
|
||||
});
|
||||
|
||||
test('BYOP uses dedicated overlay class and clears existing overlays before opening', () => {
|
||||
assert.ok(packetsSource.includes("overlay.className = 'modal-overlay byop-overlay'"),
|
||||
'BYOP overlay should have byop-overlay class');
|
||||
assert.ok(/function showBYOP\(\)\s*\{\s*removeAllByopOverlays\(\);/m.test(packetsSource),
|
||||
'showBYOP should clear existing overlays before creating a new one');
|
||||
});
|
||||
|
||||
test('BYOP close removes all overlays in one click', () => {
|
||||
assert.ok(packetsSource.includes("const close = () => { removeAllByopOverlays(); if (triggerBtn) triggerBtn.focus(); };"),
|
||||
'close handler should remove all BYOP overlays');
|
||||
});
|
||||
|
||||
test('packets page de-duplicates document click handlers', () => {
|
||||
assert.ok(packetsSource.includes("bindDocumentHandler('action', 'click'"),
|
||||
'action click handler should be bound through bindDocumentHandler');
|
||||
assert.ok(packetsSource.includes("bindDocumentHandler('menu', 'click'"),
|
||||
'menu close handler should be bound through bindDocumentHandler');
|
||||
assert.ok(packetsSource.includes("bindDocumentHandler('colmenu', 'click'"),
|
||||
'column menu close handler should be bound through bindDocumentHandler');
|
||||
assert.ok(packetsSource.includes("if (prev) document.removeEventListener(eventName, prev);"),
|
||||
'bindDocumentHandler should remove previous handler before re-binding');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== APP.JS: formatEngineBadge =====
|
||||
|
||||
Reference in New Issue
Block a user