From de85e18b81994fa5bf5172d2e1c629412d4bac0b Mon Sep 17 00:00:00 2001 From: you Date: Tue, 24 Mar 2026 19:25:28 +0000 Subject: [PATCH] fix: separate heatmap opacity controls for Map and Live pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- public/customize.js | 33 +++++++++++++++++++++++++--- public/index.html | 50 +++++++++++++++++++++--------------------- public/live.js | 8 ++++++- test-e2e-playwright.js | 25 +++++++++++++++++++++ 4 files changed, 87 insertions(+), 29 deletions(-) diff --git a/public/customize.js b/public/customize.js index 09fadee2..bca7fb9a 100644 --- a/public/customize.js +++ b/public/customize.js @@ -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 '
' + '

Node Role Colors

' + rows + '
' + '

Packet Type Colors

' + typeRows + '
' + - '

Heatmap

' + + '

Heatmap Opacity

' + '
' + - '
' + - '
Controls how opaque the heatmap overlay appears on the live map (0–100%)
' + + '
' + + '
Heatmap overlay on the Nodes β†’ Map page (0–100%)
' + '' + '' + heatPct + '%' + '
' + + '
' + + '
' + + '
Heatmap overlay on the Live page (0–100%)
' + + '' + + '' + liveHeatPct + '%' + + '
' + '
'; } @@ -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 () { diff --git a/public/index.html b/public/index.html index 12f09d72..2171639e 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -81,27 +81,27 @@
- - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/live.js b/public/live.js index bd8739be..7de2199c 100644 --- a/public/live.js +++ b/public/live.js @@ -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; } } diff --git a/test-e2e-playwright.js b/test-e2e-playwright.js index f0df3281..7050b056 100644 --- a/test-e2e-playwright.js +++ b/test-e2e-playwright.js @@ -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;