feat: detect and flag inconsistent hash sizes across adverts

Tracks all hash_size values seen per node. If a node has sent adverts
with different hash sizes, flags it as hash_size_inconsistent with a
yellow ⚠️ badge on both side pane and detail page. Tooltip mentions
likely firmware bug (pre-1.14.1). Stats row shows all sizes seen.
This commit is contained in:
you
2026-03-23 16:39:02 +00:00
parent 02b53876e9
commit da96cb6e87
2 changed files with 21 additions and 7 deletions
+3 -3
View File
@@ -119,7 +119,7 @@
body.innerHTML = `
<div class="node-full-card" style="padding:12px 16px;margin-bottom:8px">
<div class="node-detail-name" style="font-size:20px">${escapeHtml(n.name || '(unnamed)')}</div>
<div style="margin:4px 0 6px"><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span> ${n.hash_size ? `<span class="badge" style="background:var(--nav-bg);color:var(--nav-text);font-family:var(--mono)">${n.public_key.slice(0, n.hash_size * 2).toUpperCase()}</span>` : ''} ${statusLabel}</div>
<div style="margin:4px 0 6px"><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span> ${n.hash_size ? `<span class="badge" style="background:var(--nav-bg);color:var(--nav-text);font-family:var(--mono)">${n.public_key.slice(0, n.hash_size * 2).toUpperCase()}</span>` : ''} ${n.hash_size_inconsistent ? '<span class="badge" style="background:var(--status-yellow);color:#000;font-size:10px" title="Adverts show different hash sizes — likely firmware bug (pre-1.14.1)">⚠️ hash mismatch</span>' : ''} ${statusLabel}</div>
<div class="node-detail-key mono" style="font-size:11px;word-break:break-all;margin-bottom:6px">${n.public_key}</div>
<div>
<button class="btn-primary" id="copyUrlBtn" style="font-size:12px;padding:4px 10px">📋 Copy URL</button>
@@ -143,7 +143,7 @@
${stats.avgSnr != null ? `<tr><td>Avg SNR</td><td>${stats.avgSnr.toFixed(1)} dB</td></tr>` : ''}
${stats.avgHops ? `<tr><td>Avg Hops</td><td>${stats.avgHops}</td></tr>` : ''}
${hasLoc ? `<tr><td>Location</td><td>${n.lat.toFixed(5)}, ${n.lon.toFixed(5)}</td></tr>` : ''}
<tr><td>Hash Prefix</td><td>${n.hash_size ? '<code style="font-family:var(--mono);font-weight:700">' + n.public_key.slice(0, n.hash_size * 2).toUpperCase() + '</code> (' + n.hash_size + '-byte)' : 'Unknown'}</td></tr>
<tr><td>Hash Prefix</td><td>${n.hash_size ? '<code style="font-family:var(--mono);font-weight:700">' + n.public_key.slice(0, n.hash_size * 2).toUpperCase() + '</code> (' + n.hash_size + '-byte)' : 'Unknown'}${n.hash_size_inconsistent ? ' <span style="color:var(--status-yellow)" title="Seen hash sizes: ' + (n.hash_sizes_seen || []).join(', ') + '-byte">⚠️ inconsistent — possible firmware bug</span>' : ''}</td></tr>
</table>
${observers.length ? `<div class="node-full-card">
@@ -503,7 +503,7 @@
panel.innerHTML = `
<div class="node-detail">
<div class="node-detail-name">${escapeHtml(n.name || '(unnamed)')}</div>
<div class="node-detail-role"><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span> ${n.hash_size ? `<span class="badge" style="background:var(--nav-bg);color:var(--nav-text);font-family:var(--mono)">${n.public_key.slice(0, n.hash_size * 2).toUpperCase()}</span>` : ''} ${statusLabel}
<div class="node-detail-role"><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span> ${n.hash_size ? `<span class="badge" style="background:var(--nav-bg);color:var(--nav-text);font-family:var(--mono)">${n.public_key.slice(0, n.hash_size * 2).toUpperCase()}</span>` : ''} ${n.hash_size_inconsistent ? '<span class="badge" style="background:var(--status-yellow);color:#000;font-size:10px" title="Adverts show different hash sizes — likely firmware bug (pre-1.14.1)">⚠️ hash mismatch</span>' : ''} ${statusLabel}
<a href="#/nodes/${encodeURIComponent(n.public_key)}" class="btn-primary" style="display:inline-block;text-decoration:none;font-size:11px;padding:2px 8px;margin-left:8px">🔍 Details</a>
<a href="#/nodes/${encodeURIComponent(n.public_key)}/analytics" class="btn-primary" style="display:inline-block;margin-left:4px;text-decoration:none;font-size:11px;padding:2px 8px">📊 Analytics</a>
</div>
+18 -4
View File
@@ -43,9 +43,11 @@ const crypto = require('crypto');
const PacketStore = require('./packet-store');
// --- Precomputed hash_size map (updated on new packets, not per-request) ---
const _hashSizeMap = new Map();
const _hashSizeMap = new Map(); // pubkey → latest hash_size (number)
const _hashSizeAllMap = new Map(); // pubkey → Set of all hash_sizes seen
function _rebuildHashSizeMap() {
_hashSizeMap.clear();
_hashSizeAllMap.clear();
// Pass 1: from ADVERT packets (most authoritative — path byte bits 7-6)
// packets array is sorted newest-first, so first-match = newest ADVERT
for (const p of pktStore.packets) {
@@ -53,9 +55,12 @@ function _rebuildHashSizeMap() {
try {
const d = JSON.parse(p.decoded_json || '{}');
const pk = d.pubKey || d.public_key;
if (pk && !_hashSizeMap.has(pk)) {
if (pk) {
const pathByte = parseInt(p.raw_hex.slice(2, 4), 16);
_hashSizeMap.set(pk, ((pathByte >> 6) & 0x3) + 1);
const hs = ((pathByte >> 6) & 0x3) + 1;
if (!_hashSizeMap.has(pk)) _hashSizeMap.set(pk, hs);
if (!_hashSizeAllMap.has(pk)) _hashSizeAllMap.set(pk, new Set());
_hashSizeAllMap.get(pk).add(hs);
}
} catch {}
}
@@ -89,7 +94,10 @@ function _updateHashSizeForPacket(p) {
const pk = d.pubKey || d.public_key;
if (pk) {
const pathByte = parseInt(p.raw_hex.slice(2, 4), 16);
_hashSizeMap.set(pk, ((pathByte >> 6) & 0x3) + 1);
const hs = ((pathByte >> 6) & 0x3) + 1;
_hashSizeMap.set(pk, hs);
if (!_hashSizeAllMap.has(pk)) _hashSizeAllMap.set(pk, new Set());
_hashSizeAllMap.get(pk).add(hs);
}
} catch {}
} else if (p.path_json && p.decoded_json) {
@@ -1238,6 +1246,9 @@ app.get('/api/nodes', (req, res) => {
// Use precomputed hash_size map (rebuilt at startup, updated on new packets)
for (const node of nodes) {
node.hash_size = _hashSizeMap.get(node.public_key) || null;
const allSizes = _hashSizeAllMap.get(node.public_key);
node.hash_size_inconsistent = allSizes ? allSizes.size > 1 : false;
if (allSizes && allSizes.size > 1) node.hash_sizes_seen = [...allSizes].sort();
}
res.json({ nodes, total, counts });
@@ -1378,6 +1389,9 @@ app.get('/api/nodes/:pubkey', (req, res) => {
const node = db.db.prepare('SELECT * FROM nodes WHERE public_key = ?').get(pubkey);
if (!node) return res.status(404).json({ error: 'Not found' });
node.hash_size = _hashSizeMap.get(pubkey) || null;
const allSizes = _hashSizeAllMap.get(pubkey);
node.hash_size_inconsistent = allSizes ? allSizes.size > 1 : false;
if (allSizes && allSizes.size > 1) node.hash_sizes_seen = [...allSizes].sort();
const recentAdverts = (pktStore.byNode.get(pubkey) || []).slice(-20).reverse();
const _nResult = { node, recentAdverts };
cache.set(_ck, _nResult, TTL.nodeDetail);