Compare commits

..

11 Commits

Author SHA1 Message Date
you
1050aa52d9 fix: pause button delegation on document instead of app element 2026-03-22 04:59:45 +00:00
you
e8d9aa6839 v2.4.1 — hotfix for ingestion, WS, and animation regressions 2026-03-22 04:57:29 +00:00
you
daa84fdd73 fix: null-guard animLayer/pathsLayer in animation cleanup
setInterval callbacks fire after navigating away from live map,
when layers are already destroyed.
2026-03-22 04:52:45 +00:00
you
01df3c70d8 fix: null-guard multi-select menu close handler
observerFilterWrap/typeFilterWrap don't exist when document click
fires before renderLeft completes.
2026-03-22 04:49:51 +00:00
you
368fef8713 Remove debug console.log from packets WS handler 2026-03-22 04:47:10 +00:00
you
657653dd3b fix: pause button crash killed WS handler registration
getElementById('pktPauseBtn') was null because button is rendered by
loadPackets() which runs async. Crash at line 218 prevented wsHandler
from being registered at line 260. Moved to data-action event delegation
which works regardless of render timing.
2026-03-22 04:45:04 +00:00
you
11828a321a debug: add WS handler logging to packets page 2026-03-22 04:42:41 +00:00
you
f09e338dfa fix: WS broadcast had null packet when observation was deduped
getById() returns null for deduped observations (not stored in byId).
Client filters on m.data.packet being truthy, so all deduped packets
were silently dropped from WS. Fallback to transmission or raw pktData.
2026-03-22 04:38:07 +00:00
you
87672797f9 docs: add ingestion regression to v2.4.0 changelog 2026-03-22 01:23:36 +00:00
you
b337da6461 fix: packet-store insert() returned undefined after insertPacket removal
Was returning undeclared 'id' variable. Now returns observationId or
transmissionId. This broke all MQTT packet ingestion on prod.
2026-03-22 01:22:21 +00:00
you
bd32aa9565 docs: tone down v2.4.0 description 2026-03-22 01:13:47 +00:00
7 changed files with 40 additions and 25 deletions

View File

@@ -1,8 +1,19 @@
# Changelog
## [2.4.1] — 2026-03-22
Hotfix release for regressions introduced in v2.4.0.
### Fixed
- Packet ingestion broken: `insert()` returned undefined after legacy table removal, causing all MQTT packets to fail silently
- Live packet updates not working: pause button `addEventListener` on null element crashed `init()`, preventing WS handler registration
- WS broadcast had null packet data when observation was deduped (2nd+ observer of same packet)
- Multi-select filter menu close handler crashed on null `observerFilterWrap`/`typeFilterWrap` elements
- Live map animation cleanup crashed with null `animLayer`/`pathsLayer` after navigating away (setInterval kept firing)
## [2.4.0] — 2026-03-22
Big batch: observation drill-down, distance analytics, regional filters on all tabs, channel decryption fixes, performance, and a ton of bug fixes.
UI polish, client-side filtering, time window selector, DB cleanup, and bug fixes.
### Added
- Observation-level deeplinks (`#/packets/HASH?obs=OBSERVER_ID`)
@@ -70,6 +81,7 @@ Big batch: observation drill-down, distance analytics, regional filters on all t
- Feed panel position (obscured by VCR bar)
- Hop disambiguation anchored from sender origin
- Packet hash case normalization for deeplinks
- Critical: packet ingestion broken after legacy table removal (`insert()` returned undefined)
- Sort help tooltip rendering (CSS pseudo-elements don't support newlines)
### Performance

View File

@@ -1,6 +1,6 @@
{
"name": "meshcore-analyzer",
"version": "2.4.0",
"version": "2.4.1",
"description": "Community-run alternative to the closed-source `analyzer.letsmesh.net`. MQTT packet collection + open-source web analyzer for the Bay Area MeshCore mesh.",
"main": "index.js",
"scripts": {

View File

@@ -442,7 +442,7 @@ class PacketStore {
this._evict();
this.stats.inserts++;
}
return id;
return observationId || transmissionId;
}
/**

View File

@@ -84,13 +84,13 @@
<script src="hop-resolver.js?v=1774126708"></script>
<script src="app.js?v=1774126708"></script>
<script src="home.js?v=1774042199"></script>
<script src="packets.js?v=1774141832"></script>
<script src="packets.js?v=1774155585"></script>
<script src="map.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774331200" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774135052" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774155165" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774290000" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774028201" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>

View File

@@ -1685,13 +1685,12 @@
if (step >= steps) {
clearInterval(interval);
animLayer.removeLayer(dot);
if (animLayer) animLayer.removeLayer(dot);
recentPaths.push({ line, glowLine: contrail, time: Date.now() });
while (recentPaths.length > 5) {
const old = recentPaths.shift();
pathsLayer.removeLayer(old.line);
pathsLayer.removeLayer(old.glowLine);
if (pathsLayer) { pathsLayer.removeLayer(old.line); pathsLayer.removeLayer(old.glowLine); }
}
setTimeout(() => {
@@ -1700,8 +1699,7 @@
fadeOp -= 0.1;
if (fadeOp <= 0) {
clearInterval(fi);
pathsLayer.removeLayer(line);
pathsLayer.removeLayer(contrail);
if (pathsLayer) { pathsLayer.removeLayer(line); pathsLayer.removeLayer(contrail); }
recentPaths = recentPaths.filter(p => p.line !== line);
} else {
line.setStyle({ opacity: fadeOp });

View File

@@ -208,21 +208,24 @@
}
// Event delegation for data-action buttons
app.addEventListener('click', function (e) {
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
if (btn.dataset.action === 'pkt-refresh') loadPackets();
else if (btn.dataset.action === 'pkt-byop') showBYOP();
});
document.getElementById('pktPauseBtn').addEventListener('click', function() {
packetsPaused = !packetsPaused;
this.textContent = packetsPaused ? '▶' : '⏸';
this.title = packetsPaused ? 'Resume live updates' : 'Pause live updates';
this.classList.toggle('active', packetsPaused);
if (!packetsPaused && pauseBuffer.length) {
pauseBuffer.forEach(msg => wsHandler(msg));
pauseBuffer = [];
else if (btn.dataset.action === 'pkt-pause') {
packetsPaused = !packetsPaused;
const pauseBtn = document.getElementById('pktPauseBtn');
if (pauseBtn) {
pauseBtn.textContent = packetsPaused ? '▶' : '⏸';
pauseBtn.title = packetsPaused ? 'Resume live updates' : 'Pause live updates';
pauseBtn.classList.toggle('active', packetsPaused);
}
if (!packetsPaused && pauseBuffer.length) {
const handler = wsHandler;
pauseBuffer.forEach(msg => { if (handler) handler(msg); });
pauseBuffer = [];
}
}
});
@@ -454,7 +457,7 @@
<h2>Latest Packets <span class="count">(${totalCount})</span></h2>
<div>
<button class="btn-icon" data-action="pkt-refresh" title="Refresh">🔄</button>
<button class="btn-icon" id="pktPauseBtn" title="Pause live updates">⏸</button>
<button class="btn-icon" id="pktPauseBtn" data-action="pkt-pause" title="Pause live updates">⏸</button>
<button class="btn-icon" data-action="pkt-byop" title="Bring Your Own Packet" aria-label="Bring Your Own Packet - paste raw packet hex for analysis" aria-haspopup="dialog">📦 BYOP</button>
</div>
</div>
@@ -608,8 +611,10 @@
// Close multi-select menus on outside click
document.addEventListener('click', (e) => {
if (!document.getElementById('observerFilterWrap').contains(e.target)) obsMenu.classList.remove('open');
if (!document.getElementById('typeFilterWrap').contains(e.target)) typeMenu.classList.remove('open');
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'); }
if (typeWrap && !typeWrap.contains(e.target)) { const m = typeWrap.querySelector('.multi-select-menu'); if (m) m.classList.remove('open'); }
});
// Filter toggle button for mobile

View File

@@ -730,7 +730,7 @@ for (const source of mqttSources) {
// Invalidate caches on new data
cache.debouncedInvalidateAll();
const fullPacket = pktStore.getById(packetId);
const fullPacket = pktStore.getById(packetId) || pktStore.byHash.get(pktData.hash) || pktData;
const tx = pktStore.byHash.get(pktData.hash);
const observation_count = tx ? tx.observation_count : 1;
const broadcastData = { id: packetId, raw: msg.raw, decoded, snr: msg.SNR, rssi: msg.RSSI, hash: pktData.hash, observer: observerId, packet: fullPacket, observation_count };