mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 12:44:12 +00:00
perf: replace server round-trip with client-side filter for My Nodes toggle (#401)
## Summary Fixes #381 — The "My Nodes" filter in `packets.js` was making a **server API call inside `renderTableRows()`** on every render cycle. With WebSocket updates arriving every few seconds while the toggle was active, this created continuous unnecessary server load. ## What Changed **`public/packets.js`** — Replaced the `api('/packets?nodes=...')` server call with a pure client-side filter: ```js // Before: server round-trip on every render const myData = await api('/packets?nodes=' + allKeys.join(',') + '&limit=500'); displayPackets = myData.packets || []; // After: filter already-loaded packets client-side displayPackets = displayPackets.filter(p => { const dj = p.decoded_json || ''; return allKeys.some(k => dj.includes(k)); }); ``` This uses the exact same matching logic as the server's `QueryMultiNodePackets()` — a string contains check on `decoded_json` for each pubkey — but without the network round-trip. **`test-frontend-helpers.js`** — Added 5 unit tests for the filter logic: - Single and multiple pubkey matching - No matches / empty keys edge case - Null/empty `decoded_json` handled gracefully **`public/index.html`** — Cache busters bumped. ## Test Results - Frontend helpers: **232 passed, 0 failed** (including 5 new tests) - Packet filter: **62 passed, 0 failed** - Aging: **29 passed, 0 failed** Co-authored-by: you <you@example.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=1775022775">
|
||||
<link rel="stylesheet" href="home.css?v=1775022775">
|
||||
<link rel="stylesheet" href="live.css?v=1775022775">
|
||||
<link rel="stylesheet" href="style.css?v=1775055000">
|
||||
<link rel="stylesheet" href="home.css?v=1775055000">
|
||||
<link rel="stylesheet" href="live.css?v=1775055000">
|
||||
<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=1775022775"></script>
|
||||
<script src="customize.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775022775"></script>
|
||||
<script src="hop-resolver.js?v=1775022775"></script>
|
||||
<script src="hop-display.js?v=1775022775"></script>
|
||||
<script src="app.js?v=1775022775"></script>
|
||||
<script src="home.js?v=1775022775"></script>
|
||||
<script src="packet-filter.js?v=1775022775"></script>
|
||||
<script src="packets.js?v=1775022775"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775022775"></script>
|
||||
<script src="map.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1775055000"></script>
|
||||
<script src="customize.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775055000"></script>
|
||||
<script src="hop-resolver.js?v=1775055000"></script>
|
||||
<script src="hop-display.js?v=1775055000"></script>
|
||||
<script src="app.js?v=1775055000"></script>
|
||||
<script src="home.js?v=1775055000"></script>
|
||||
<script src="packet-filter.js?v=1775055000"></script>
|
||||
<script src="packets.js?v=1775055000"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775055000"></script>
|
||||
<script src="map.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775055000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-5
@@ -997,7 +997,7 @@
|
||||
const groupBtn = document.getElementById('fGroup');
|
||||
if (groupBtn) groupBtn.classList.toggle('active', groupByHash);
|
||||
|
||||
// Filter to claimed/favorited nodes if toggle is on — use server-side multi-node lookup
|
||||
// Filter to claimed/favorited nodes — pure client-side filter (no server round-trip)
|
||||
let displayPackets = packets;
|
||||
if (filters.myNodes) {
|
||||
const myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]');
|
||||
@@ -1005,10 +1005,10 @@
|
||||
const favs = getFavorites();
|
||||
const allKeys = [...new Set([...myKeys, ...favs])];
|
||||
if (allKeys.length > 0) {
|
||||
try {
|
||||
const myData = await api('/packets?nodes=' + allKeys.join(',') + '&limit=500');
|
||||
displayPackets = myData.packets || [];
|
||||
} catch { displayPackets = []; }
|
||||
displayPackets = displayPackets.filter(p => {
|
||||
const dj = p.decoded_json || '';
|
||||
return allKeys.some(k => dj.includes(k));
|
||||
});
|
||||
} else {
|
||||
displayPackets = [];
|
||||
}
|
||||
|
||||
@@ -2642,6 +2642,56 @@ console.log('\n=== packets.js: savedTimeWindowMin defaults ===');
|
||||
assert.ok(deltaMin > 10 && deltaMin < 25, `expected capped ~15m window, got ${deltaMin.toFixed(2)}m`);
|
||||
});
|
||||
}
|
||||
// ===== My Nodes client-side filter (issue #381) =====
|
||||
{
|
||||
console.log('\n--- My Nodes client-side filter ---');
|
||||
|
||||
// Simulate the client-side filter logic from packets.js renderTableRows()
|
||||
function filterMyNodes(packets, allKeys) {
|
||||
if (!allKeys.length) return [];
|
||||
return packets.filter(p => {
|
||||
const dj = p.decoded_json || '';
|
||||
return allKeys.some(k => dj.includes(k));
|
||||
});
|
||||
}
|
||||
|
||||
const testPackets = [
|
||||
{ decoded_json: '{"pubKey":"abc123","name":"Node1"}' },
|
||||
{ decoded_json: '{"pubKey":"def456","name":"Node2"}' },
|
||||
{ decoded_json: '{"pubKey":"ghi789","name":"Node3","hops":["abc123"]}' },
|
||||
{ decoded_json: '' },
|
||||
{ decoded_json: null },
|
||||
];
|
||||
|
||||
test('filters packets matching a single pubkey', () => {
|
||||
const result = filterMyNodes(testPackets, ['abc123']);
|
||||
assert.strictEqual(result.length, 2, 'should match sender + hop');
|
||||
assert.ok(result[0].decoded_json.includes('abc123'));
|
||||
assert.ok(result[1].decoded_json.includes('abc123'));
|
||||
});
|
||||
|
||||
test('filters packets matching multiple pubkeys', () => {
|
||||
const result = filterMyNodes(testPackets, ['abc123', 'def456']);
|
||||
assert.strictEqual(result.length, 3);
|
||||
});
|
||||
|
||||
test('returns empty array for no matching keys', () => {
|
||||
const result = filterMyNodes(testPackets, ['zzz999']);
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('returns empty array when allKeys is empty', () => {
|
||||
const result = filterMyNodes(testPackets, []);
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('handles null/empty decoded_json gracefully', () => {
|
||||
const result = filterMyNodes(testPackets, ['abc123']);
|
||||
// Should not throw, null decoded_json packets are skipped
|
||||
assert.strictEqual(result.length, 2);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== SUMMARY =====
|
||||
Promise.allSettled(pendingTests).then(() => {
|
||||
console.log(`\n${'═'.repeat(40)}`);
|
||||
|
||||
Reference in New Issue
Block a user