From b114cd6eb045be5dc9715f849acff2e8d24a66a3 Mon Sep 17 00:00:00 2001 From: you Date: Sat, 21 Mar 2026 00:19:15 +0000 Subject: [PATCH] Add hash size labels for repeater markers on map - Compute hash_size from ADVERT packets in /api/nodes response - Show colored rectangle markers with hash size (e.g. '2B') for repeaters - Add 'Hash size labels' toggle in map controls (default ON, saved to localStorage) - Non-repeater markers unchanged --- public/index.html | 2 +- public/map.js | 26 ++++++++++++++++++++++++-- server.js | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 4a6565b8..82e7e3b6 100644 --- a/public/index.html +++ b/public/index.html @@ -83,7 +83,7 @@ - + diff --git a/public/map.js b/public/map.js index 3bdea471..9c47b374 100644 --- a/public/map.js +++ b/public/map.js @@ -8,7 +8,7 @@ let clusterGroup = null; let nodes = []; let observers = []; - let filters = { repeater: true, companion: true, room: true, sensor: true, observer: true, lastHeard: '30d', neighbors: false, clusters: false }; + let filters = { repeater: true, companion: true, room: true, sensor: true, observer: true, lastHeard: '30d', neighbors: false, clusters: false, hashLabels: localStorage.getItem('meshcore-map-hash-labels') !== 'false' }; let wsHandler = null; let heatLayer = null; let userHasMoved = false; @@ -60,6 +60,20 @@ }); } + function makeRepeaterLabelIcon(node) { + var hashSize = node.hash_size || 1; + var s = ROLE_STYLE['repeater'] || ROLE_STYLE.companion; + var label = hashSize + 'B'; + var html = '
' + label + '
'; + return L.divIcon({ + html: html, + className: 'meshcore-marker meshcore-label-marker', + iconSize: null, + iconAnchor: [14, 12], + popupAnchor: [0, -12], + }); + } + function init(container) { container.innerHTML = `
@@ -79,6 +93,7 @@
Filters +
Last Heard @@ -154,6 +169,13 @@ document.getElementById('mcClusters').addEventListener('change', e => { filters.clusters = e.target.checked; renderMarkers(); }); document.getElementById('mcHeatmap').addEventListener('change', e => { toggleHeatmap(e.target.checked); }); document.getElementById('mcNeighbors').addEventListener('change', e => { filters.neighbors = e.target.checked; renderMarkers(); }); + + // Hash Labels toggle + const hashLabelEl = document.getElementById('mcHashLabels'); + if (hashLabelEl) { + hashLabelEl.checked = filters.hashLabels; + hashLabelEl.addEventListener('change', e => { filters.hashLabels = e.target.checked; localStorage.setItem('meshcore-map-hash-labels', filters.hashLabels); renderMarkers(); }); + } document.getElementById('mcLastHeard').addEventListener('change', e => { filters.lastHeard = e.target.value; loadNodes(); }); // WS for live advert updates @@ -355,7 +377,7 @@ }); for (const node of filtered) { - const icon = makeMarkerIcon(node.role || 'companion'); + const icon = (node.role === 'repeater' && filters.hashLabels) ? makeRepeaterLabelIcon(node) : makeMarkerIcon(node.role || 'companion'); const marker = L.marker([node.lat, node.lon], { icon, alt: `${node.name || 'Unknown'} (${node.role || 'node'})`, diff --git a/server.js b/server.js index 903a5bf3..f04c5a94 100644 --- a/server.js +++ b/server.js @@ -973,6 +973,24 @@ app.get('/api/nodes', (req, res) => { counts[r + 's'] = db.db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE role = ?`).get(r).count; } + // Compute hash_size for each node from latest ADVERT packets + const hashSizeMap = new Map(); + for (const p of pktStore.packets) { + if (p.payload_type === 4 && p.decoded_json) { + try { + const d = JSON.parse(p.decoded_json); + const pk = d.pubKey || d.public_key; + if (pk && p.raw_hex && !hashSizeMap.has(pk)) { + const pathByte = parseInt(p.raw_hex.slice(2, 4), 16); + hashSizeMap.set(pk, ((pathByte >> 6) & 0x3) + 1); + } + } catch {} + } + } + for (const node of nodes) { + node.hash_size = hashSizeMap.get(node.public_key) || null; + } + res.json({ nodes, total, counts }); });