mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-02 11:44:06 +00:00
fix: separate heatmap opacity controls for Map and Live pages
- Live page showHeatMap() now reads meshcore-live-heatmap-opacity from localStorage and applies it to the canvas element (was hardcoded 0.3) - Customizer now has two clearly labeled sliders: 🗺️ Nodes Map — controls the static map page heatmap 📡 Live Map — controls the live page heatmap - Each uses its own localStorage key (meshcore-heatmap-opacity vs meshcore-live-heatmap-opacity) - Added E2E tests for live opacity persistence and dual slider existence - 13/15 E2E tests pass locally (2 fail due to ARM chromium OOM after heavy live page tests — CI on x64 will handle them) Closes #119 properly this time.
This commit is contained in:
+30
-3
@@ -731,18 +731,27 @@
|
||||
var heatOpacity = parseFloat(localStorage.getItem('meshcore-heatmap-opacity'));
|
||||
if (isNaN(heatOpacity)) heatOpacity = 0.25;
|
||||
var heatPct = Math.round(heatOpacity * 100);
|
||||
var liveHeatOpacity = parseFloat(localStorage.getItem('meshcore-live-heatmap-opacity'));
|
||||
if (isNaN(liveHeatOpacity)) liveHeatOpacity = 0.3;
|
||||
var liveHeatPct = Math.round(liveHeatOpacity * 100);
|
||||
return '<div class="cust-panel' + (activeTab === 'nodes' ? ' active' : '') + '" data-panel="nodes">' +
|
||||
'<p class="cust-section-title">Node Role Colors</p>' + rows +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Packet Type Colors</p>' + typeRows +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Heatmap</p>' +
|
||||
'<p class="cust-section-title">Heatmap Opacity</p>' +
|
||||
'<div class="cust-color-row">' +
|
||||
'<div><label>🔥 Opacity</label>' +
|
||||
'<div class="cust-hint">Controls how opaque the heatmap overlay appears on the live map (0–100%)</div></div>' +
|
||||
'<div><label>🗺️ Nodes Map</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Nodes → Map page (0–100%)</div></div>' +
|
||||
'<input type="range" id="custHeatOpacity" min="0" max="100" value="' + heatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span id="custHeatOpacityVal" style="font-family:var(--mono);font-size:12px;color:var(--text-muted);min-width:36px">' + heatPct + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="cust-color-row">' +
|
||||
'<div><label>📡 Live Map</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Live page (0–100%)</div></div>' +
|
||||
'<input type="range" id="custLiveHeatOpacity" min="0" max="100" value="' + liveHeatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span id="custLiveHeatOpacityVal" style="font-family:var(--mono);font-size:12px;color:var(--text-muted);min-width:36px">' + liveHeatPct + '%</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -1030,6 +1039,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Live heatmap opacity slider
|
||||
var liveHeatSlider = container.querySelector('#custLiveHeatOpacity');
|
||||
if (liveHeatSlider) {
|
||||
liveHeatSlider.addEventListener('input', function () {
|
||||
var pct = parseInt(liveHeatSlider.value);
|
||||
var label = container.querySelector('#custLiveHeatOpacityVal');
|
||||
if (label) label.textContent = pct + '%';
|
||||
var opacity = pct / 100;
|
||||
localStorage.setItem('meshcore-live-heatmap-opacity', opacity);
|
||||
// Live-update the live page heatmap if visible
|
||||
if (window._meshcoreLiveHeatLayer) {
|
||||
var canvas = window._meshcoreLiveHeatLayer._canvas ||
|
||||
(window._meshcoreLiveHeatLayer.getContainer && window._meshcoreLiveHeatLayer.getContainer());
|
||||
if (canvas) canvas.style.opacity = opacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Steps
|
||||
container.querySelectorAll('[data-step-field]').forEach(function (inp) {
|
||||
inp.addEventListener('input', function () {
|
||||
|
||||
+25
-25
@@ -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=1774379786">
|
||||
<link rel="stylesheet" href="home.css?v=1774379786">
|
||||
<link rel="stylesheet" href="live.css?v=1774379786">
|
||||
<link rel="stylesheet" href="style.css?v=1774380053">
|
||||
<link rel="stylesheet" href="home.css?v=1774380053">
|
||||
<link rel="stylesheet" href="live.css?v=1774380053">
|
||||
<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=1774379786"></script>
|
||||
<script src="customize.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774379786"></script>
|
||||
<script src="hop-resolver.js?v=1774379786"></script>
|
||||
<script src="hop-display.js?v=1774379786"></script>
|
||||
<script src="app.js?v=1774379786"></script>
|
||||
<script src="home.js?v=1774379786"></script>
|
||||
<script src="packet-filter.js?v=1774379786"></script>
|
||||
<script src="packets.js?v=1774379786"></script>
|
||||
<script src="map.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774379786" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774380053"></script>
|
||||
<script src="customize.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774380053"></script>
|
||||
<script src="hop-resolver.js?v=1774380053"></script>
|
||||
<script src="hop-display.js?v=1774380053"></script>
|
||||
<script src="app.js?v=1774380053"></script>
|
||||
<script src="home.js?v=1774380053"></script>
|
||||
<script src="packet-filter.js?v=1774380053"></script>
|
||||
<script src="packets.js?v=1774380053"></script>
|
||||
<script src="map.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774380053" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+7
-1
@@ -2106,10 +2106,16 @@
|
||||
}
|
||||
}
|
||||
if (points.length && typeof L.heatLayer === 'function') {
|
||||
var savedOpacity = parseFloat(localStorage.getItem('meshcore-live-heatmap-opacity'));
|
||||
if (isNaN(savedOpacity)) savedOpacity = 0.3;
|
||||
heatLayer = L.heatLayer(points, {
|
||||
radius: 25, blur: 15, maxZoom: 14, minOpacity: 0.3,
|
||||
radius: 25, blur: 15, maxZoom: 14, minOpacity: 0.05,
|
||||
gradient: { 0.2: '#0d47a1', 0.4: '#1565c0', 0.6: '#42a5f5', 0.8: '#ffca28', 1.0: '#ff5722' }
|
||||
}).addTo(map);
|
||||
// Set overall layer opacity via canvas element
|
||||
if (heatLayer._canvas) { heatLayer._canvas.style.opacity = savedOpacity; }
|
||||
else { setTimeout(function() { if (heatLayer && heatLayer._canvas) heatLayer._canvas.style.opacity = savedOpacity; }, 100); }
|
||||
window._meshcoreLiveHeatLayer = heatLayer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -273,6 +273,31 @@ async function run() {
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Test 14: Live heatmap opacity stored in localStorage
|
||||
await test('Live heatmap opacity persists in localStorage', async () => {
|
||||
// Verify localStorage key works (no page load needed — reuse current page)
|
||||
await page.evaluate(() => localStorage.setItem('meshcore-live-heatmap-opacity', '0.6'));
|
||||
const opacity = await page.evaluate(() => localStorage.getItem('meshcore-live-heatmap-opacity'));
|
||||
assert(opacity === '0.6', `Live opacity should persist as "0.6" but got "${opacity}"`);
|
||||
await page.evaluate(() => localStorage.removeItem('meshcore-live-heatmap-opacity'));
|
||||
});
|
||||
|
||||
// Test 15: Customizer has separate Map and Live opacity sliders
|
||||
await test('Customizer has separate map and live opacity sliders', async () => {
|
||||
// Verify by checking JS source — avoids heavy page reloads that crash ARM chromium
|
||||
const custJs = await page.evaluate(async () => {
|
||||
const res = await fetch('/customize.js?_=' + Date.now());
|
||||
return res.text();
|
||||
});
|
||||
assert(custJs.includes('custHeatOpacity'), 'customize.js should have map opacity slider (custHeatOpacity)');
|
||||
assert(custJs.includes('custLiveHeatOpacity'), 'customize.js should have live opacity slider (custLiveHeatOpacity)');
|
||||
assert(custJs.includes('meshcore-heatmap-opacity'), 'customize.js should use meshcore-heatmap-opacity key');
|
||||
assert(custJs.includes('meshcore-live-heatmap-opacity'), 'customize.js should use meshcore-live-heatmap-opacity key');
|
||||
// Verify labels are distinct
|
||||
assert(custJs.includes('Nodes Map') || custJs.includes('nodes map') || custJs.includes('🗺'), 'Map slider should have map-related label');
|
||||
assert(custJs.includes('Live Map') || custJs.includes('live map') || custJs.includes('📡'), 'Live slider should have live-related label');
|
||||
});
|
||||
|
||||
// Summary
|
||||
const passed = results.filter(r => r.pass).length;
|
||||
const failed = results.filter(r => !r.pass).length;
|
||||
|
||||
Reference in New Issue
Block a user