diff --git a/public/analytics.js b/public/analytics.js index 49a7b909..091ab0e7 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -6,6 +6,14 @@ const sf = (v, d) => (v != null ? v.toFixed(d) : '–'); // safe toFixed function esc(s) { return s ? String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"') : ''; } + // --- Status color helpers (read from CSS variables for theme support) --- + function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } + function statusGreen() { return cssVar('--status-green') || '#22c55e'; } + function statusYellow() { return cssVar('--status-yellow') || '#eab308'; } + function statusRed() { return cssVar('--status-red') || '#ef4444'; } + function accentColor() { return cssVar('--accent') || '#4a9eff'; } + function snrColor(snr) { return snr > 6 ? statusGreen() : snr > 0 ? statusYellow() : statusRed(); } + // --- SVG helpers --- function sparkSvg(data, color, w = 120, h = 32) { if (!data.length) return ''; @@ -247,8 +255,8 @@ // ===================== RF / SIGNAL ===================== function renderRF(el, rf) { - const snrHist = histogram(rf.snrValues, 20, '#22c55e'); - const rssiHist = histogram(rf.rssiValues, 20, '#3b82f6'); + const snrHist = histogram(rf.snrValues, 20, statusGreen()); + const rssiHist = histogram(rf.rssiValues, 20, accentColor()); el.innerHTML = `
@@ -329,20 +337,21 @@ svg += `${rssi}`; } // Quality zones + const _sg = statusGreen(), _sy = statusYellow(), _sr = statusRed(); const zones = [ - { label: 'Excellent', snr: [6, 15], rssi: [-80, -5], color: '#22c55e20' }, - { label: 'Good', snr: [0, 6], rssi: [-100, -80], color: '#f59e0b15' }, - { label: 'Weak', snr: [-12, 0], rssi: [-130, -100], color: '#ef444410' }, + { label: 'Excellent', snr: [6, 15], rssi: [-80, -5], color: _sg + '20' }, + { label: 'Good', snr: [0, 6], rssi: [-100, -80], color: _sy + '15' }, + { label: 'Weak', snr: [-12, 0], rssi: [-130, -100], color: _sr + '10' }, ]; // Define patterns for color-blind accessibility svg += ``; - svg += ``; - svg += ``; - svg += ``; + svg += ``; + svg += ``; + svg += ``; svg += ``; const zonePatterns = { 'Excellent': 'pat-excellent', 'Good': 'pat-good', 'Weak': 'pat-weak' }; const zoneDash = { 'Excellent': '4,2', 'Good': '6,3', 'Weak': '2,2' }; - const zoneBorder = { 'Excellent': '#22c55e', 'Good': '#f59e0b', 'Weak': '#ef4444' }; + const zoneBorder = { 'Excellent': _sg, 'Good': _sy, 'Weak': _sr }; zones.forEach(z => { const x1 = pad + (z.snr[0] - snrMin) / (snrMax - snrMin) * (w - pad * 2); const x2 = pad + (z.snr[1] - snrMin) / (snrMax - snrMin) * (w - pad * 2); @@ -369,7 +378,7 @@ let html = ''; snrByType.forEach(t => { const barPct = Math.max(((t.avg - (-12)) / 27) * 100, 2); - const color = t.avg > 6 ? '#22c55e' : t.avg > 0 ? '#f59e0b' : '#ef4444'; + const color = t.avg > 6 ? statusGreen() : t.avg > 0 ? statusYellow() : statusRed(); html += ` @@ -392,7 +401,7 @@ const y = h - pad - ((d.avgSnr + 12) / 27) * (h - pad * 2); return `${x},${y}`; }).join(' '); - svg += ``; + svg += ``; // Packet count as area const areaPts = data.map((d, i) => { const x = pad + i * ((w - pad * 2) / Math.max(data.length - 1, 1)); @@ -411,7 +420,7 @@ svg += `${data[i].hour.slice(11)}h`; } svg += ''; - svg += `
Avg SNRVolume
`; + svg += `
Avg SNRVolume
`; return svg; } @@ -526,7 +535,7 @@ const x = pad + (d.hops / maxHop) * (w - pad * 2); const y = h - pad - ((d.avgSnr + 12) / 27) * (h - pad * 2); const r = Math.min(Math.sqrt(d.count) * 1.5, 12); - const color = d.avgSnr > 6 ? '#22c55e' : d.avgSnr > 0 ? '#f59e0b' : '#ef4444'; + const color = d.avgSnr > 6 ? statusGreen() : d.avgSnr > 0 ? statusYellow() : statusRed(); svg += ``; svg += `${d.hops}h`; }); @@ -927,13 +936,13 @@
${collisions.map(c => { let badge, tooltip; if (c.classification === 'local') { - badge = '🏘️ Local'; + badge = '🏘️ Local'; tooltip = 'Nodes close enough for direct RF — probably genuine prefix collision'; } else if (c.classification === 'regional') { - badge = '⚡ Regional'; + badge = '⚡ Regional'; tooltip = 'At edge of 915MHz range — could indicate atmospheric ducting or hilltop-to-hilltop links'; } else if (c.classification === 'distant') { - badge = '🌐 Distant'; + badge = '🌐 Distant'; tooltip = 'Beyond typical LoRa range — likely internet bridging, MQTT gateway, or separate mesh networks sharing prefix'; } else { badge = '❓ Unknown'; @@ -993,7 +1002,7 @@ - + `; }).join('')}
TypePacketsAvg SNRMinMaxDistribution
${t.name} ${t.count}
${routeDisplay}${hasSelfLoop ? ' 🔄' : ''}
${esc(prefixDisplay)}
${s.count.toLocaleString()} ${s.pct}%
`; @@ -1093,7 +1102,7 @@ const dLon = (a.lon - b.lon) * 85; const km = Math.sqrt(dLat*dLat + dLon*dLon); total += km; - const cls = km > 200 ? 'color:#ef4444;font-weight:bold' : km > 50 ? 'color:#f59e0b' : 'color:#22c55e'; + const cls = km > 200 ? 'color:var(--status-red);font-weight:bold' : km > 50 ? 'color:var(--status-yellow)' : 'color:var(--status-green)'; dists.push(`
${km < 1 ? (km*1000).toFixed(0)+'m' : km.toFixed(1)+'km'} ${esc(a.name)} → ${esc(b.name)}
`); } else { dists.push(`
? ${esc(a.name)} → ${esc(b.name)} (no coords)
`); @@ -1155,13 +1164,13 @@ const isEnd = i === 0 || i === nodesWithLoc.length - 1; L.circleMarker(ll, { radius: isEnd ? 8 : 5, - color: isEnd ? (i === 0 ? '#22c55e' : '#ef4444') : '#f59e0b', - fillColor: isEnd ? (i === 0 ? '#22c55e' : '#ef4444') : '#f59e0b', + color: isEnd ? (i === 0 ? statusGreen() : statusRed()) : statusYellow(), + fillColor: isEnd ? (i === 0 ? statusGreen() : statusRed()) : statusYellow(), fillOpacity: 0.9, weight: 2 }).bindTooltip(n.name, { permanent: false }).addTo(map); }); - L.polyline(latlngs, { color: '#f59e0b', weight: 3, dashArray: '8,6', opacity: 0.8 }).addTo(map); + L.polyline(latlngs, { color: statusYellow(), weight: 3, dashArray: '8,6', opacity: 0.8 }).addTo(map); map.fitBounds(L.latLngBounds(latlngs).pad(0.3)); } } @@ -1207,15 +1216,15 @@

🔍 Network Status

-
${active}
+
${active}
🟢 Active
-
${degraded}
+
${degraded}
🟡 Degraded
-
${silent}
+
${silent}
🔴 Silent
@@ -1340,7 +1349,7 @@ if (data.distHistogram && data.distHistogram.bins) { const buckets = data.distHistogram.bins.map(b => b.count); const labels = data.distHistogram.bins.map(b => b.x.toFixed(1)); - html += `

Hop Distance Distribution

${barChart(buckets, labels, '#22c55e')}
`; + html += `

Hop Distance Distribution

${barChart(buckets, labels, statusGreen())}
`; } // Distance over time diff --git a/public/live.css b/public/live.css index 9db0257d..d1d7af3f 100644 --- a/public/live.css +++ b/public/live.css @@ -50,7 +50,7 @@ .live-beacon { width: 8px; height: 8px; - background: #ef4444; + background: var(--status-red); border-radius: 50%; display: inline-block; animation: beaconPulse 1.5s ease-in-out infinite; @@ -541,7 +541,7 @@ } .vcr-lcd-mode { font-size: 0.65rem; - color: #4ade80; + color: var(--status-green); text-shadow: 0 0 6px rgba(74, 222, 128, 0.6); font-weight: 700; } diff --git a/public/live.js b/public/live.js index 66b98f10..9404e4c3 100644 --- a/public/live.js +++ b/public/live.js @@ -1,6 +1,10 @@ (function() { 'use strict'; + // Status color helpers (read from CSS variables for theme support) + function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } + function statusGreen() { return cssVar('--status-green') || '#22c55e'; } + let map, ws, nodesLayer, pathsLayer, animLayer, heatLayer; let nodeMarkers = {}; let nodeData = {}; @@ -348,7 +352,7 @@ const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); - drawLcdText(`${hh}:${mm}:${ss}`, '#4ade80'); + drawLcdText(`${hh}:${mm}:${ss}`, statusGreen()); } function updateVCRLcd() { @@ -644,11 +648,11 @@

PACKET TYPES

    -
  • Advert — Node advertisement
  • -
  • Message — Group text
  • -
  • Direct — Direct message
  • -
  • Request — Data request
  • -
  • Trace — Route trace
  • +
  • Advert — Node advertisement
  • +
  • Message — Group text
  • +
  • Direct — Direct message
  • +
  • Request — Data request
  • +
  • Trace — Route trace

NODE ROLES

    diff --git a/public/style.css b/public/style.css index fa55a6de..ee42fe07 100644 --- a/public/style.css +++ b/public/style.css @@ -114,7 +114,7 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible 70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); } 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } } -.live-dot.connected { background: #22c55e; animation: pulse-ring 2s ease-out infinite; } +.live-dot.connected { background: var(--status-green); animation: pulse-ring 2s ease-out infinite; } .nav-links { display: flex; align-items: center; gap: 4px; } .nav-link { @@ -1231,7 +1231,7 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); } .hop-conflict-dist { font-size: 11px; color: var(--text-muted); font-family: var(--mono); white-space: nowrap; } .hop-conflict-pk { font-size: 10px; color: var(--text-muted); font-family: var(--mono); } .hop-unreliable { opacity: 0.5; text-decoration: line-through; } -.hop-global-fallback { border-bottom: 1px dashed #ef4444; } +.hop-global-fallback { border-bottom: 1px dashed var(--status-red); } .hop-current { font-weight: 700 !important; color: var(--accent) !important; } /* Self-loop subpath rows */ @@ -1468,9 +1468,9 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } .perf-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; } .perf-table code { font-size: 12px; color: var(--text); } .perf-table .perf-slow { background: rgba(239, 68, 68, 0.08); } -.perf-table .perf-slow td { color: #ef4444; } +.perf-table .perf-slow td { color: var(--status-red); } .perf-table .perf-warn { background: rgba(251, 191, 36, 0.06); } -.perf-table .perf-warn td { color: #f59e0b; } +.perf-table .perf-warn td { color: var(--status-yellow); } /* ─── Region filter bar ─── */ .region-filter-bar { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 0; }