fix: null-guard pathHops to prevent detail pane crash (#451) (#454)

## Summary

Fixes #451 — packet detail pane crash on direct routed packets where
`pathHops` is `null`.

## Root Cause

`JSON.parse(pkt.path_json)` can return literal `null` when the DB stores
`"null"` for direct routed packets. The existing code only had a catch
block for parse errors, but `null` is valid JSON — so the parse succeeds
and `pathHops` ends up `null` instead of `[]`.

## Changes

- **`public/packets.js`**: Added `|| []` after `JSON.parse(...)` in both
`buildFlatRowHtml` (table rows) and the detail pane (`selectPacket`),
ensuring `pathHops` is always an array.
- **`test-frontend-helpers.js`**: Added 2 regression tests verifying the
null guards exist in both code paths.
- **`public/index.html`**: Cache buster bump.

## Testing

- All 229 frontend helper tests pass
- All 62 packet filter tests pass
- All 29 aging tests pass

Co-authored-by: you <you@example.com>
This commit is contained in:
Kpa-clawbot
2026-04-01 10:48:08 -07:00
committed by GitHub
parent 044a5387af
commit ec4dd58cb6
3 changed files with 46 additions and 30 deletions
+28 -28
View File
@@ -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=1775060497">
<link rel="stylesheet" href="home.css?v=1775060497">
<link rel="stylesheet" href="live.css?v=1775060497">
<link rel="stylesheet" href="style.css?v=1775062162">
<link rel="stylesheet" href="home.css?v=1775062162">
<link rel="stylesheet" href="live.css?v=1775062162">
<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=1775060497"></script>
<script src="customize.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1775060497"></script>
<script src="hop-resolver.js?v=1775060497"></script>
<script src="hop-display.js?v=1775060497"></script>
<script src="app.js?v=1775060497"></script>
<script src="home.js?v=1775060497"></script>
<script src="packet-filter.js?v=1775060497"></script>
<script src="packets.js?v=1775060497"></script>
<script src="geo-filter-overlay.js?v=1775060497"></script>
<script src="map.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1775060497" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1775062162"></script>
<script src="customize.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1775062162"></script>
<script src="hop-resolver.js?v=1775062162"></script>
<script src="hop-display.js?v=1775062162"></script>
<script src="app.js?v=1775062162"></script>
<script src="home.js?v=1775062162"></script>
<script src="packet-filter.js?v=1775062162"></script>
<script src="packets.js?v=1775062162"></script>
<script src="geo-filter-overlay.js?v=1775062162"></script>
<script src="map.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1775062162" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+2 -2
View File
@@ -1080,7 +1080,7 @@
function buildFlatRowHtml(p) {
let decoded, pathHops = [];
try { decoded = JSON.parse(p.decoded_json || '{}'); } catch {}
try { pathHops = JSON.parse(p.path_json || '[]'); } catch {}
try { pathHops = JSON.parse(p.path_json || '[]') || []; } catch {}
const region = p.observer_id ? (observers.find(o => o.id === p.observer_id)?.iata || '') : '';
const typeName = payloadTypeName(p.payload_type);
const typeClass = payloadTypeColor(p.payload_type);
@@ -1420,7 +1420,7 @@
let decoded;
try { decoded = JSON.parse(pkt.decoded_json); } catch { decoded = {}; }
let pathHops;
try { pathHops = JSON.parse(pkt.path_json || '[]'); } catch { pathHops = []; }
try { pathHops = JSON.parse(pkt.path_json || '[]') || []; } catch { pathHops = []; }
// Resolve sender GPS — from packet directly, or from known node in DB
let senderLat = decoded.lat != null ? decoded.lat : (decoded.latitude || null);
+16
View File
@@ -2668,6 +2668,22 @@ console.log('\n=== packets.js: savedTimeWindowMin defaults ===');
'buildFlatRowHtml should have null-safe decoded_json fallback');
});
test('pathHops null guard in buildFlatRowHtml (issue #451)', () => {
const flatBuilderMatch = packetsSource.match(/function buildFlatRowHtml[\s\S]*?(?=\n function )/);
assert.ok(flatBuilderMatch, 'buildFlatRowHtml should exist');
// The JSON.parse result must be coalesced with || [] to handle literal null from path_json
assert.ok(flatBuilderMatch[0].includes("|| '[]') || []"),
'buildFlatRowHtml should coalesce parsed path_json with || [] to guard against null');
});
test('pathHops null guard in detail pane (issue #451)', () => {
// The detail pane (selectPacket / showPacketDetail) also parses path_json
const detailMatch = packetsSource.match(/let pathHops;\s*try \{[^}]+\} catch/);
assert.ok(detailMatch, 'detail pane pathHops parsing should exist');
assert.ok(detailMatch[0].includes("|| '[]') || []"),
'detail pane should coalesce parsed path_json with || [] to guard against null');
});
test('destroy cleans up virtual scroll state', () => {
assert.ok(packetsSource.includes('detachVScrollListener'),
'destroy should detach virtual scroll listener');