mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-25 10:52:08 +00:00
fix: null-guard animLayer and liveAnimCount in nextHop after destroy (#462)
## Summary - `nextHop()` schedules `setInterval`/`setTimeout` callbacks that can fire after `destroy()` has set `animLayer = null` and removed DOM elements - This caused three console errors on the Live page when navigating away mid-animation: `Cannot read properties of null (reading 'hasLayer')` and `Cannot set properties of null (setting 'textContent')` - Added null guards at each async callback site; no behavioral change when the page is active ## Changes - `public/live.js`: early return if `animLayer` is null at start of `nextHop()`; null-safe `animLayer.hasLayer` checks in `setInterval`/`setTimeout`; null-safe `liveAnimCount` element access - `public/index.html`: cache buster bumped - `test-frontend-helpers.js`: 4 source-inspection tests verifying the null guards are present ## Test plan - [ ] Open Live page, trigger some packet animations, navigate away quickly — no console errors - [ ] `node test-frontend-helpers.js` passes (233 tests, 0 failures) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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=1775077121">
|
||||
<link rel="stylesheet" href="home.css?v=1775077121">
|
||||
<link rel="stylesheet" href="live.css?v=1775077121">
|
||||
<link rel="stylesheet" href="style.css?v=1775078686">
|
||||
<link rel="stylesheet" href="home.css?v=1775078686">
|
||||
<link rel="stylesheet" href="live.css?v=1775078686">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -85,30 +85,30 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1775077121"></script>
|
||||
<script src="customize.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775077121"></script>
|
||||
<script src="hop-resolver.js?v=1775077121"></script>
|
||||
<script src="hop-display.js?v=1775077121"></script>
|
||||
<script src="app.js?v=1775077121"></script>
|
||||
<script src="home.js?v=1775077121"></script>
|
||||
<script src="packet-filter.js?v=1775077121"></script>
|
||||
<script src="packets.js?v=1775077121"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775077121"></script>
|
||||
<script src="map.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775077121" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1775078686"></script>
|
||||
<script src="customize.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775078686"></script>
|
||||
<script src="hop-resolver.js?v=1775078686"></script>
|
||||
<script src="hop-display.js?v=1775078686"></script>
|
||||
<script src="app.js?v=1775078686"></script>
|
||||
<script src="home.js?v=1775078686"></script>
|
||||
<script src="packet-filter.js?v=1775078686"></script>
|
||||
<script src="packets.js?v=1775078686"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775078686"></script>
|
||||
<script src="map.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775078686" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1850,9 +1850,11 @@
|
||||
function nextHop() {
|
||||
if (hopIndex >= hopPositions.length) {
|
||||
activeAnims = Math.max(0, activeAnims - 1);
|
||||
document.getElementById('liveAnimCount').textContent = activeAnims;
|
||||
const countEl = document.getElementById('liveAnimCount');
|
||||
if (countEl) countEl.textContent = activeAnims;
|
||||
return;
|
||||
}
|
||||
if (!animLayer) return;
|
||||
// Audio hook: notify per-hop callback
|
||||
if (onHop) try { onHop(hopIndex, hopPositions.length, hopPositions[hopIndex]); } catch (e) {}
|
||||
const hp = hopPositions[hopIndex];
|
||||
@@ -1865,11 +1867,11 @@
|
||||
}).addTo(animLayer);
|
||||
let pulseUp = true;
|
||||
const pulseTimer = setInterval(() => {
|
||||
if (!animLayer.hasLayer(ghost)) { clearInterval(pulseTimer); return; }
|
||||
if (!animLayer || !animLayer.hasLayer(ghost)) { clearInterval(pulseTimer); return; }
|
||||
ghost.setStyle({ fillOpacity: pulseUp ? 0.6 : 0.25, opacity: pulseUp ? 0.7 : 0.4 });
|
||||
pulseUp = !pulseUp;
|
||||
}, 600);
|
||||
setTimeout(() => { clearInterval(pulseTimer); if (animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost); }, 3000);
|
||||
setTimeout(() => { clearInterval(pulseTimer); if (animLayer && animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost); }, 3000);
|
||||
}
|
||||
} else {
|
||||
pulseNode(hp.key, hp.pos, typeName);
|
||||
|
||||
@@ -2788,6 +2788,34 @@ console.log('\n=== packets.js: savedTimeWindowMin defaults ===');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== live.js: nextHop null guards =====
|
||||
console.log('\n=== live.js: nextHop null guards ===');
|
||||
{
|
||||
const liveSource = fs.readFileSync('public/live.js', 'utf8');
|
||||
|
||||
test('nextHop guards animLayer null before use', () => {
|
||||
assert.ok(liveSource.includes('if (!animLayer) return;'),
|
||||
'nextHop must return early when animLayer is null (post-destroy)');
|
||||
});
|
||||
|
||||
test('nextHop setInterval guards animLayer null', () => {
|
||||
assert.ok(liveSource.includes('if (!animLayer || !animLayer.hasLayer(ghost))'),
|
||||
'setInterval in nextHop must guard animLayer null');
|
||||
});
|
||||
|
||||
test('nextHop setTimeout guards animLayer null', () => {
|
||||
assert.ok(liveSource.includes('if (animLayer && animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost)'),
|
||||
'setTimeout in nextHop must guard animLayer null');
|
||||
});
|
||||
|
||||
test('nextHop guards liveAnimCount element null', () => {
|
||||
assert.ok(liveSource.includes('const countEl = document.getElementById(\'liveAnimCount\')'),
|
||||
'nextHop must null-check liveAnimCount element');
|
||||
assert.ok(liveSource.includes('if (countEl) countEl.textContent = activeAnims'),
|
||||
'nextHop must conditionally update liveAnimCount');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== SUMMARY =====
|
||||
Promise.allSettled(pendingTests).then(() => {
|
||||
console.log(`\n${'═'.repeat(40)}`);
|
||||
|
||||
Reference in New Issue
Block a user