diff --git a/public/analytics.js b/public/analytics.js index 7ada19f..e5e4f27 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -753,8 +753,6 @@ async function renderHashMatrix(topHops) { const el = document.getElementById('hashMatrix'); - const oneByteHops = topHops.filter(h => h.size === 1); - if (!oneByteHops.length) { el.innerHTML = '
No 1-byte hops
'; return; } // Fetch all nodes for lookup let allNodes = []; @@ -763,23 +761,18 @@ allNodes = nd.nodes || []; } catch {} - // Build 16x16 grid - const grid = Array.from({ length: 16 }, () => Array(16).fill(0)); - let maxCount = 0; - for (const hop of oneByteHops) { - const byte = parseInt(hop.hex, 16); - if (isNaN(byte)) continue; - const hi = (byte >> 4) & 0xF; - const lo = byte & 0xF; - grid[hi][lo] = hop.count; - if (hop.count > maxCount) maxCount = hop.count; + // Build prefix → node count map + const prefixNodes = {}; + for (let i = 0; i < 256; i++) { + const hex = i.toString(16).padStart(2, '0').toUpperCase(); + prefixNodes[hex] = allNodes.filter(n => n.public_key.toUpperCase().startsWith(hex)); } const nibbles = '0123456789ABCDEF'.split(''); const cellSize = 36; const headerSize = 24; - let html = `
`; + let html = `
`; html += ``; for (const n of nibbles) { html += ``; @@ -789,38 +782,47 @@ for (let hi = 0; hi < 16; hi++) { html += ``; for (let lo = 0; lo < 16; lo++) { - const count = grid[hi][lo]; const hex = nibbles[hi] + nibbles[lo]; - let bg = 'transparent'; - let color = 'var(--text-muted)'; - if (count > 0) { - const intensity = Math.log(count + 1) / Math.log(maxCount + 1); - const r = Math.round(34 + intensity * (239 - 34)); - const g = Math.round(197 - intensity * (197 - 68)); - const b = Math.round(94 - intensity * (94 - 68)); - bg = `rgb(${r},${g},${b})`; - color = intensity > 0.5 ? '#fff' : 'var(--text)'; + const nodes = prefixNodes[hex] || []; + const count = nodes.length; + let bg, color; + if (count === 0) { + bg = 'var(--bg-card, #1a1a2e)'; color = 'var(--text-muted)'; + } else if (count === 1) { + bg = '#166534'; color = '#fff'; // green — free, single user + } else { + // 2+ nodes: interpolate yellow→red based on count + const t = Math.min((count - 2) / 4, 1); // 2=yellow, 6+=full red + const r = 239; + const g = Math.round(180 * (1 - t)); + bg = `rgb(${r},${g},50)`; color = '#fff'; } - const nodeCount = allNodes.filter(n => n.public_key.toLowerCase().startsWith(hex.toLowerCase())).length; - html += ``; + const status = count === 0 ? 'unused' : count === 1 ? `1 node: ${nodes[0].name || nodes[0].public_key.slice(0,12)}` : `${count} nodes — COLLISION`; + html += ``; } html += ''; } html += '
${n}
${nibbles[hi]}${count || ''}${hex}
'; - html += '
'; + html += `
+
+ Unused + 1 node (free) + 2 nodes + 3+ nodes (collision) +
`; el.innerHTML = html; // Click handler for cells el.querySelectorAll('.hash-active').forEach(td => { td.addEventListener('click', () => { - const hex = td.dataset.hex.toLowerCase(); - const matches = allNodes.filter(n => n.public_key.toLowerCase().startsWith(hex)); + const hex = td.dataset.hex.toUpperCase(); + const matches = prefixNodes[hex] || []; const detail = document.getElementById('hashDetail'); if (!matches.length) { - detail.innerHTML = `0x${hex.toUpperCase()}
No known nodes`; + detail.innerHTML = `0x${hex}
No known nodes`; return; } - detail.innerHTML = `0x${hex.toUpperCase()} — ${matches.length} node${matches.length !== 1 ? 's' : ''}` + + detail.innerHTML = `0x${hex} — ${matches.length} node${matches.length !== 1 ? 's' : ''}` + `
${matches.map(m => { const coords = (m.lat && m.lon && !(m.lat === 0 && m.lon === 0)) ? `(${m.lat.toFixed(2)}, ${m.lon.toFixed(2)})` @@ -828,7 +830,6 @@ const role = m.role ? `${esc(m.role)} ` : ''; return `
${role}${esc(m.name || m.public_key.slice(0,12))} ${coords}
`; }).join('')}
`; - // Highlight selected cell el.querySelectorAll('.hash-selected').forEach(c => c.classList.remove('hash-selected')); td.classList.add('hash-selected'); });