fix: cast snr/rssi to Number before toFixed() — fixes crash on string values

Observer detail, home health timeline, and traces all called
.toFixed() on snr/rssi values that may be strings from the DB.
Wrapping in Number() matches what live.js already does.
This commit is contained in:
you
2026-03-24 20:17:41 +00:00
parent 14ff1821d6
commit d6ea3dd9fd
4 changed files with 31 additions and 31 deletions

View File

@@ -287,7 +287,7 @@
<div class="mnc-lbl">Observers</div>
</div>
<div class="mnc-metric">
<div class="mnc-val" style="color:${snrColor}">${snrVal != null ? snrVal.toFixed(1) + ' dB' : '—'}</div>
<div class="mnc-val" style="color:${snrColor}">${snrVal != null ? Number(snrVal).toFixed(1) + ' dB' : '—'}</div>
<div class="mnc-lbl">SNR${snrLabel ? ' · ' + snrLabel : ''}</div>
</div>
<div class="mnc-metric">
@@ -425,7 +425,7 @@
<div class="health-metric"><div class="val">${stats.packetsToday ?? '—'}</div><div class="lbl">Packets Today</div></div>
<div class="health-metric"><div class="val">${observers.length}</div><div class="lbl">Observers</div></div>
<div class="health-metric"><div class="val">${stats.lastHeard ? timeAgo(stats.lastHeard) : '—'}</div><div class="lbl">Last seen</div></div>
<div class="health-metric"><div class="val">${snrVal != null ? snrVal.toFixed(1) + ' dB' : '—'}</div><div class="lbl">Avg SNR${snrLabel ? ' · ' + snrLabel : ''}</div></div>
<div class="health-metric"><div class="val">${snrVal != null ? Number(snrVal).toFixed(1) + ' dB' : '—'}</div><div class="lbl">Avg SNR${snrLabel ? ' · ' + snrLabel : ''}</div></div>
<div class="health-metric"><div class="val">${stats.avgHops != null ? stats.avgHops.toFixed(1) : '—'}</div><div class="lbl">Avg Hops</div></div>
</div>
${observers.length ? `<div class="health-observers"><strong>Heard by:</strong> ${observers.map(o => escapeHtml(o.observer_name || o.observer_id)).join(', ')}</div>` : ''}
@@ -444,7 +444,7 @@
<span class="badge" style="background:var(--type-${payloadTypeColor(p.payload_type)})">${escapeHtml(payloadTypeName(p.payload_type))}</span>
<span>via ${escapeHtml(obsId)}</span>
<span class="time">${timeAgo(p.timestamp || p.created_at)}</span>
<span class="snr">${p.snr != null ? p.snr.toFixed(1) + ' dB' : ''}</span>
<span class="snr">${p.snr != null ? Number(p.snr).toFixed(1) + ' dB' : ''}</span>
</div>`;
}).join('') : '<p style="color:var(--text-muted);font-size:.85rem">No recent packets found for this node.</p>'}
</div>

View File

@@ -22,9 +22,9 @@
<meta name="twitter:title" content="MeshCore Analyzer">
<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/meshcore-analyzer/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774380547">
<link rel="stylesheet" href="home.css?v=1774380547">
<link rel="stylesheet" href="live.css?v=1774380547">
<link rel="stylesheet" href="style.css?v=1774383461">
<link rel="stylesheet" href="home.css?v=1774383461">
<link rel="stylesheet" href="live.css?v=1774383461">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -81,27 +81,27 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1774380547"></script>
<script src="customize.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774380547"></script>
<script src="hop-resolver.js?v=1774380547"></script>
<script src="hop-display.js?v=1774380547"></script>
<script src="app.js?v=1774380547"></script>
<script src="home.js?v=1774380547"></script>
<script src="packet-filter.js?v=1774380547"></script>
<script src="packets.js?v=1774380547"></script>
<script src="map.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774380547" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1774383461"></script>
<script src="customize.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774383461"></script>
<script src="hop-resolver.js?v=1774383461"></script>
<script src="hop-display.js?v=1774383461"></script>
<script src="app.js?v=1774383461"></script>
<script src="home.js?v=1774383461"></script>
<script src="packet-filter.js?v=1774383461"></script>
<script src="packets.js?v=1774383461"></script>
<script src="map.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774383461" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>

View File

@@ -308,7 +308,7 @@
<td>${timeAgo(p.timestamp)}</td>
<td>${typeName}</td>
<td class="mono" style="font-size:0.85em">${(p.hash || '').substring(0, 10)}</td>
<td>${p.snr != null ? p.snr.toFixed(1) : '—'}</td>
<td>${p.snr != null ? Number(p.snr).toFixed(1) : '—'}</td>
<td>${p.rssi != null ? p.rssi : '—'}</td>
<td>${hops.length}</td>
</tr>`;

View File

@@ -280,8 +280,8 @@
<div class="tl-marker" style="left:${pct}%" title="${time.toISOString()}"></div>
</div>
<div class="tl-delta mono">${delta}</div>
<div class="tl-snr ${snrClass}">${t.snr != null ? t.snr.toFixed(1) + ' dB' : '—'}</div>
<div class="tl-rssi">${t.rssi != null ? t.rssi.toFixed(0) + ' dBm' : '—'}</div>
<div class="tl-snr ${snrClass}">${t.snr != null ? Number(t.snr).toFixed(1) + ' dB' : '—'}</div>
<div class="tl-rssi">${t.rssi != null ? Number(t.rssi).toFixed(0) + ' dBm' : '—'}</div>
</div>`;
});