mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-31 14:35:43 +00:00
Compare commits
2 Commits
v3.1.0
...
fix/byop-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81f4beda43 | ||
|
|
0b82ca791e |
@@ -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=1774923001">
|
||||
<link rel="stylesheet" href="home.css?v=1774923001">
|
||||
<link rel="stylesheet" href="live.css?v=1774923001">
|
||||
<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=1774923001"></script>
|
||||
<script src="customize.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774923001"></script>
|
||||
<script src="hop-resolver.js?v=1774923001"></script>
|
||||
<script src="hop-display.js?v=1774923001"></script>
|
||||
<script src="app.js?v=1774923001"></script>
|
||||
<script src="home.js?v=1774923001"></script>
|
||||
<script src="packet-filter.js?v=1774923001"></script>
|
||||
<script src="packets.js?v=1774923001"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774923001"></script>
|
||||
<script src="map.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774923001" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -157,9 +157,33 @@
|
||||
let directPacketId = null;
|
||||
let directPacketHash = null;
|
||||
let initGeneration = 0;
|
||||
let _docActionHandler = null;
|
||||
let _docMenuCloseHandler = null;
|
||||
let _docColMenuCloseHandler = null;
|
||||
let _docEscHandler = 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
|
||||
: kind === 'colmenu'
|
||||
? _docColMenuCloseHandler
|
||||
: _docEscHandler;
|
||||
if (prev) document.removeEventListener(eventName, prev);
|
||||
document.addEventListener(eventName, handler);
|
||||
if (kind === 'action') _docActionHandler = handler;
|
||||
else if (kind === 'menu') _docMenuCloseHandler = handler;
|
||||
else if (kind === 'colmenu') _docColMenuCloseHandler = handler;
|
||||
else _docEscHandler = handler;
|
||||
}
|
||||
|
||||
function renderTimestampCell(isoString) {
|
||||
if (typeof formatTimestampWithTooltip !== 'function' || typeof getTimestampMode !== 'function') {
|
||||
return escapeHtml(typeof timeAgo === 'function' ? timeAgo(isoString) : '—');
|
||||
@@ -237,7 +261,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 +400,11 @@
|
||||
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; }
|
||||
if (_docEscHandler) { document.removeEventListener('keydown', _docEscHandler); _docEscHandler = null; }
|
||||
removeAllByopOverlays();
|
||||
packets = [];
|
||||
hashIndex = new Map(); selectedId = null;
|
||||
filtersBuilt = false;
|
||||
@@ -705,7 +734,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 +849,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 () {
|
||||
@@ -943,7 +972,7 @@
|
||||
}
|
||||
|
||||
// Escape to close packet detail panel
|
||||
document.addEventListener('keydown', function pktEsc(e) {
|
||||
bindDocumentHandler('esc', 'keydown', function pktEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeDetailPanel();
|
||||
}
|
||||
@@ -1548,9 +1577,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 +1591,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 +1627,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>';
|
||||
|
||||
@@ -1300,6 +1300,9 @@ console.log('\n=== compare.js: comparePacketSets ===');
|
||||
{
|
||||
console.log('\nPackets page — detail pane initial state:');
|
||||
const packetsSource = fs.readFileSync('public/packets.js', 'utf8');
|
||||
const removeAllByopOverlaysSource = extractFunctionSourceFromText(packetsSource, 'removeAllByopOverlays');
|
||||
const showBYOPSource = extractFunctionSourceFromText(packetsSource, 'showBYOP');
|
||||
const bindDocumentHandlerSource = extractFunctionSourceFromText(packetsSource, 'bindDocumentHandler');
|
||||
|
||||
test('split-layout starts with detail-collapsed class', () => {
|
||||
// The template literal that creates the split-layout must include detail-collapsed
|
||||
@@ -1318,6 +1321,198 @@ 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');
|
||||
});
|
||||
|
||||
test('BYOP repeated opens keep exactly one overlay', () => {
|
||||
assert.ok(removeAllByopOverlaysSource, 'removeAllByopOverlays source should be present');
|
||||
assert.ok(showBYOPSource, 'showBYOP source should be present');
|
||||
const ctx = vm.createContext({
|
||||
document: createPacketsTestDocument(),
|
||||
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
|
||||
renderDecodedPacket: () => '',
|
||||
routeTypeName: () => 'UNKNOWN',
|
||||
payloadTypeName: () => 'UNKNOWN',
|
||||
});
|
||||
vm.runInContext(`${removeAllByopOverlaysSource}\n${showBYOPSource}`, ctx);
|
||||
ctx.showBYOP();
|
||||
ctx.showBYOP();
|
||||
assert.strictEqual(ctx.document.getOverlayCount(), 1, 'repeated opens should leave one overlay');
|
||||
});
|
||||
|
||||
test('BYOP close removes all overlays', () => {
|
||||
assert.ok(removeAllByopOverlaysSource, 'removeAllByopOverlays source should be present');
|
||||
assert.ok(showBYOPSource, 'showBYOP source should be present');
|
||||
const ctx = vm.createContext({
|
||||
document: createPacketsTestDocument(),
|
||||
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
|
||||
renderDecodedPacket: () => '',
|
||||
routeTypeName: () => 'UNKNOWN',
|
||||
payloadTypeName: () => 'UNKNOWN',
|
||||
});
|
||||
vm.runInContext(`${removeAllByopOverlaysSource}\n${showBYOPSource}`, ctx);
|
||||
ctx.showBYOP();
|
||||
// Simulate stale stacked overlay from prior bad state; close should clear all.
|
||||
ctx.document.appendOverlay();
|
||||
assert.strictEqual(ctx.document.getOverlayCount(), 2, 'setup should contain two overlays');
|
||||
ctx.document.getFirstOverlay().querySelector('.byop-x').onclick();
|
||||
assert.strictEqual(ctx.document.getOverlayCount(), 0, 'close should remove all BYOP overlays');
|
||||
});
|
||||
|
||||
test('bindDocumentHandler removes previous handlers across SPA re-init', () => {
|
||||
assert.ok(bindDocumentHandlerSource, 'bindDocumentHandler source should be present');
|
||||
const doc = createBindingTestDocument();
|
||||
const ctx = vm.createContext({
|
||||
document: doc,
|
||||
_docActionHandler: null,
|
||||
_docMenuCloseHandler: null,
|
||||
_docColMenuCloseHandler: null,
|
||||
_docEscHandler: null,
|
||||
});
|
||||
vm.runInContext(bindDocumentHandlerSource, ctx);
|
||||
|
||||
let clicks = 0;
|
||||
const clickHandlerV1 = () => { clicks += 1; };
|
||||
const clickHandlerV2 = () => { clicks += 1; };
|
||||
ctx.bindDocumentHandler('action', 'click', clickHandlerV1);
|
||||
ctx.bindDocumentHandler('action', 'click', clickHandlerV2);
|
||||
doc.dispatch('click', { type: 'click' });
|
||||
assert.strictEqual(clicks, 1, 'only latest click handler should fire once');
|
||||
assert.strictEqual(doc.getRemoveCount('click'), 1, 'rebind should remove prior click handler');
|
||||
|
||||
let esc = 0;
|
||||
const escHandlerV1 = () => { esc += 1; };
|
||||
const escHandlerV2 = () => { esc += 1; };
|
||||
ctx.bindDocumentHandler('esc', 'keydown', escHandlerV1);
|
||||
ctx.bindDocumentHandler('esc', 'keydown', escHandlerV2);
|
||||
doc.dispatch('keydown', { key: 'Escape' });
|
||||
assert.strictEqual(esc, 1, 'only latest esc handler should fire once');
|
||||
assert.strictEqual(doc.getRemoveCount('keydown'), 1, 'rebind should remove prior keydown handler');
|
||||
});
|
||||
}
|
||||
|
||||
function extractFunctionSourceFromText(source, functionName) {
|
||||
const start = source.indexOf(`function ${functionName}(`);
|
||||
if (start === -1) return null;
|
||||
let braceStart = source.indexOf('{', start);
|
||||
if (braceStart === -1) return null;
|
||||
let depth = 0;
|
||||
for (let i = braceStart; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
if (ch === '{') depth += 1;
|
||||
else if (ch === '}') depth -= 1;
|
||||
if (depth === 0) return source.slice(start, i + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createPacketsTestDocument() {
|
||||
let overlays = [];
|
||||
let activeElement = null;
|
||||
|
||||
function createFocusable() {
|
||||
const focusable = {
|
||||
onclick: null,
|
||||
addEventListener: () => {},
|
||||
focus: () => { activeElement = focusable; }
|
||||
};
|
||||
return focusable;
|
||||
}
|
||||
|
||||
function createOverlay() {
|
||||
let removed = false;
|
||||
const closeBtn = createFocusable();
|
||||
const decodeBtn = createFocusable();
|
||||
const textarea = createFocusable();
|
||||
textarea.value = '';
|
||||
const result = { innerHTML: '' };
|
||||
const modal = {
|
||||
querySelectorAll: () => [textarea, decodeBtn, closeBtn],
|
||||
};
|
||||
const overlayObj = {
|
||||
className: '',
|
||||
innerHTML: '',
|
||||
addEventListener: () => {},
|
||||
querySelector: (sel) => {
|
||||
if (sel === '.byop-modal') return modal;
|
||||
if (sel === '.byop-x') return closeBtn;
|
||||
if (sel === '#byopHex') return textarea;
|
||||
if (sel === '#byopDecode') return decodeBtn;
|
||||
if (sel === '#byopResult') return result;
|
||||
return null;
|
||||
},
|
||||
remove: () => {
|
||||
removed = true;
|
||||
overlays = overlays.filter(o => o !== overlayObj);
|
||||
},
|
||||
__removed: () => removed,
|
||||
};
|
||||
return overlayObj;
|
||||
}
|
||||
|
||||
const triggerBtn = { focus: () => { activeElement = triggerBtn; } };
|
||||
|
||||
return {
|
||||
body: {
|
||||
appendChild: (el) => { overlays.push(el); }
|
||||
},
|
||||
querySelector: (sel) => (sel === '[data-action="pkt-byop"]' ? triggerBtn : null),
|
||||
querySelectorAll: (sel) => {
|
||||
if (sel !== '.byop-overlay') return [];
|
||||
return overlays.filter(o => !o.__removed || !o.__removed());
|
||||
},
|
||||
createElement: () => createOverlay(),
|
||||
appendOverlay: function () {
|
||||
const extra = this.createElement('div');
|
||||
extra.className = 'modal-overlay byop-overlay';
|
||||
this.body.appendChild(extra);
|
||||
return extra;
|
||||
},
|
||||
getFirstOverlay: () => overlays[0],
|
||||
getOverlayCount: () => overlays.length,
|
||||
getLastOverlay: () => overlays[overlays.length - 1],
|
||||
get activeElement() { return activeElement; },
|
||||
};
|
||||
}
|
||||
|
||||
function createBindingTestDocument() {
|
||||
const listeners = new Map();
|
||||
const removeCounts = new Map();
|
||||
return {
|
||||
addEventListener: (event, handler) => {
|
||||
listeners.set(event, handler);
|
||||
},
|
||||
removeEventListener: (event, handler) => {
|
||||
if (listeners.get(event) === handler) listeners.delete(event);
|
||||
removeCounts.set(event, (removeCounts.get(event) || 0) + 1);
|
||||
},
|
||||
dispatch: (event, payload) => {
|
||||
const handler = listeners.get(event);
|
||||
if (handler) handler(payload || {});
|
||||
},
|
||||
getRemoveCount: (event) => removeCounts.get(event) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== APP.JS: formatEngineBadge =====
|
||||
|
||||
Reference in New Issue
Block a user