mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-03 19:55:53 +00:00
Compare commits
3 Commits
docs/relea
...
fix/nodes-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6efbe2c9be | ||
|
|
4628acf946 | ||
|
|
5020e48d22 |
@@ -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=1775062162">
|
||||
<link rel="stylesheet" href="home.css?v=1775062162">
|
||||
<link rel="stylesheet" href="live.css?v=1775062162">
|
||||
<link rel="stylesheet" href="style.css?v=1775092976">
|
||||
<link rel="stylesheet" href="home.css?v=1775092976">
|
||||
<link rel="stylesheet" href="live.css?v=1775092976">
|
||||
<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=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>
|
||||
<script src="roles.js?v=1775092976"></script>
|
||||
<script src="customize.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775092976"></script>
|
||||
<script src="hop-resolver.js?v=1775092976"></script>
|
||||
<script src="hop-display.js?v=1775092976"></script>
|
||||
<script src="app.js?v=1775092976"></script>
|
||||
<script src="home.js?v=1775092976"></script>
|
||||
<script src="packet-filter.js?v=1775092976"></script>
|
||||
<script src="packets.js?v=1775092976"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775092976"></script>
|
||||
<script src="map.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775092976" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -228,11 +228,45 @@
|
||||
loadNodes();
|
||||
// Auto-refresh when ADVERT packets arrive via WebSocket (fixes #131)
|
||||
wsHandler = debouncedOnWS(function (msgs) {
|
||||
if (msgs.some(isAdvertMessage)) {
|
||||
_allNodes = null;
|
||||
const advertMsgs = msgs.filter(isAdvertMessage);
|
||||
if (!advertMsgs.length) return;
|
||||
|
||||
if (!_allNodes) {
|
||||
invalidateApiCache('/nodes');
|
||||
loadNodes(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a Map for O(1) lookup instead of O(n) .find() per advert
|
||||
var nodesByKey = new Map();
|
||||
for (var i = 0; i < _allNodes.length; i++) {
|
||||
if (_allNodes[i].public_key) nodesByKey.set(_allNodes[i].public_key, _allNodes[i]);
|
||||
}
|
||||
|
||||
let needReload = false;
|
||||
for (const m of advertMsgs) {
|
||||
const payload = m.data && m.data.decoded && m.data.decoded.payload;
|
||||
const pubKey = payload && (payload.pubKey || payload.public_key);
|
||||
if (!pubKey) { needReload = true; break; }
|
||||
|
||||
const existing = nodesByKey.get(pubKey);
|
||||
if (existing) {
|
||||
if (payload.name) existing.name = payload.name;
|
||||
if (payload.lat != null) existing.lat = payload.lat;
|
||||
if (payload.lon != null) existing.lon = payload.lon;
|
||||
const ts = m.data.packet && (m.data.packet.timestamp || m.data.packet.first_seen);
|
||||
if (ts) existing.last_seen = ts;
|
||||
} else {
|
||||
needReload = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needReload) {
|
||||
_allNodes = null;
|
||||
invalidateApiCache('/nodes');
|
||||
}
|
||||
loadNodes(true);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -929,4 +963,6 @@
|
||||
|
||||
// Test hooks
|
||||
window._nodesIsAdvertMessage = isAdvertMessage;
|
||||
window._nodesGetAllNodes = function() { return _allNodes; };
|
||||
window._nodesSetAllNodes = function(n) { _allNodes = n; };
|
||||
})();
|
||||
|
||||
@@ -1181,6 +1181,61 @@ console.log('\n=== nodes.js: WS handler runtime behavior ===');
|
||||
assert.ok(env.getApiCalls() > 0, 'api called because _allNodes was reset to null');
|
||||
});
|
||||
|
||||
test('ADVERT for known node upserts in-place without API fetch', () => {
|
||||
const env = makeNodesWsSandbox();
|
||||
// Pre-populate _allNodes with a known node
|
||||
assert.ok(typeof env.ctx.window._nodesSetAllNodes === 'function', '_nodesSetAllNodes must be exposed');
|
||||
env.ctx.window._nodesSetAllNodes([
|
||||
{ public_key: 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899', name: 'OldName', role: 'repeater', lat: null, lon: null, last_seen: '2024-01-01T00:00:00Z' }
|
||||
]);
|
||||
env.resetCounters();
|
||||
|
||||
env.sendWS({
|
||||
type: 'packet',
|
||||
data: {
|
||||
packet: { payload_type: 4, timestamp: '2024-06-01T12:00:00Z' },
|
||||
decoded: {
|
||||
header: { payloadTypeName: 'ADVERT' },
|
||||
payload: { type: 'ADVERT', pubKey: 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899', name: 'NewName', lat: 50.85, lon: 4.35 }
|
||||
}
|
||||
}
|
||||
});
|
||||
env.fireTimers();
|
||||
|
||||
assert.strictEqual(env.getApiCalls(), 0, 'known node upsert must NOT trigger API fetch');
|
||||
assert.strictEqual(env.getInvalidated().length, 0, 'no cache invalidation for known node upsert');
|
||||
const nodes = env.ctx.window._nodesGetAllNodes();
|
||||
assert.ok(nodes, '_nodesGetAllNodes must be exposed');
|
||||
assert.strictEqual(nodes[0].name, 'NewName', 'name must be updated in place');
|
||||
assert.strictEqual(nodes[0].lat, 50.85, 'lat must be updated in place');
|
||||
assert.strictEqual(nodes[0].lon, 4.35, 'lon must be updated in place');
|
||||
assert.strictEqual(nodes[0].last_seen, '2024-06-01T12:00:00Z', 'last_seen must be updated from packet timestamp');
|
||||
});
|
||||
|
||||
test('ADVERT for unknown node falls back to full reload', () => {
|
||||
const env = makeNodesWsSandbox();
|
||||
env.ctx.window._nodesSetAllNodes([
|
||||
{ public_key: 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899', name: 'ExistingNode', role: 'repeater' }
|
||||
]);
|
||||
env.resetCounters();
|
||||
|
||||
// Send ADVERT from a pubKey NOT in _allNodes
|
||||
env.sendWS({
|
||||
type: 'packet',
|
||||
data: {
|
||||
packet: { payload_type: 4 },
|
||||
decoded: {
|
||||
header: { payloadTypeName: 'ADVERT' },
|
||||
payload: { type: 'ADVERT', pubKey: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', name: 'BrandNewNode' }
|
||||
}
|
||||
}
|
||||
});
|
||||
env.fireTimers();
|
||||
|
||||
assert.ok(env.getApiCalls() > 0, 'unknown node must trigger full reload');
|
||||
assert.ok(env.getInvalidated().includes('/nodes'), 'cache must be invalidated for unknown node');
|
||||
});
|
||||
|
||||
test('scroll position and selection preserved during WS-triggered refresh', () => {
|
||||
const env = makeNodesWsSandbox();
|
||||
// Simulate scrolled panel state — WS handler should not touch scroll or rebuild panel
|
||||
|
||||
Reference in New Issue
Block a user