From 47b8f0c004873aa3345a42556ac1e02e39e6556b Mon Sep 17 00:00:00 2001 From: Christian Chwala Date: Thu, 12 Feb 2026 21:15:12 +0100 Subject: [PATCH 1/4] Enhance chart container resizing functionality and update styles --- webserver/templates/realtime.html | 114 ++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/webserver/templates/realtime.html b/webserver/templates/realtime.html index 99bac05..d3676d9 100644 --- a/webserver/templates/realtime.html +++ b/webserver/templates/realtime.html @@ -59,10 +59,33 @@ } .chart-container { - height: 500px; + height: 400px; flex-shrink: 0; position: relative; width: 100%; + min-height: 200px; + max-height: 80vh; + } + + .resizer { + height: 12px; + background: #ddd; + cursor: ns-resize; + position: relative; + z-index: 10; + border-top: 2px solid #bbb; + border-bottom: 2px solid #bbb; + margin: -2px 0; + /* Extend clickable area */ + transition: background-color 0.2s ease; + } + + .resizer:hover { + background: #aaa; + } + + .resizer.active { + background: #888; } .grafana-container { @@ -83,6 +106,7 @@
+
@@ -123,6 +123,7 @@ let cmlStats = {}; let selectedCmlId = '10001'; let currentColoringOption = 'completeness'; + let lastTimeRange = null; // Store last known time range from URL // Color functions for each option const colorFunctions = { @@ -378,21 +379,63 @@ console.log('Selected CML: ' + cmlId); selectedCmlId = cmlId; - // Update Grafana iframe URL with new CML ID, preserving current time range var grafanaPanel = document.getElementById('grafana-panel'); - var base = '/grafana/d/cml-realtime/cml-real-time-data'; - var params = [ - 'orgId=1', - 'var-cml_id=' + encodeURIComponent(cmlId), - 'refresh=10s', - 'theme=light', - 'viewPanel=2', - 'kiosk' - ]; - grafanaPanel.src = base + '?' + params.join('&'); - } - // Initialize on page load + try { + // Access the iframe's window + var iframeWindow = grafanaPanel.contentWindow; + var currentUrl = new URL(iframeWindow.location.href); + + // Update only the cml_id parameter + currentUrl.searchParams.set('var-cml_id', cmlId); + + // Use history.pushState to update URL without reload + iframeWindow.history.pushState(null, '', currentUrl.toString()); + + // Dispatch a popstate event to trigger Grafana's variable refresh + var popStateEvent = new PopStateEvent('popstate', { state: null }); + iframeWindow.dispatchEvent(popStateEvent); + + console.log('Updated CML ID via pushState'); + } catch (e) { + console.warn('Could not access iframe (cross-origin?), using fallback', e); + + // Fallback: extract time range and rebuild URL + var currentSrc = grafanaPanel.src; + var fromParam = null; + var toParam = null; + + try { + var url = new URL(currentSrc); + fromParam = url.searchParams.get('from'); + toParam = url.searchParams.get('to'); + + if (fromParam && toParam) { + lastTimeRange = { from: fromParam, to: toParam }; + } + } catch (e2) { + console.warn('Could not parse iframe URL', e2); + } + + // Build new URL + var base = '/grafana/d/cml-realtime/cml-real-time-data'; + var params = [ + 'orgId=1', + 'var-cml_id=' + encodeURIComponent(cmlId), + 'refresh=10s', + 'theme=light', + 'viewPanel=2', + 'kiosk' + ]; + + if (lastTimeRange && lastTimeRange.from && lastTimeRange.to) { + params.push('from=' + encodeURIComponent(lastTimeRange.from)); + params.push('to=' + encodeURIComponent(lastTimeRange.to)); + } + + grafanaPanel.src = base + '?' + params.join('&'); + } + } // Initialize on page load document.addEventListener('DOMContentLoaded', function () { initializeMap(); initializeResizer(); From 6b89741fd4bd6a30314338c703b18b8b1a9799f4 Mon Sep 17 00:00:00 2001 From: Christian Chwala Date: Thu, 12 Feb 2026 21:55:30 +0100 Subject: [PATCH 3/4] refactor: improve CML selection and iframe URL handling in Grafana integration --- webserver/templates/realtime.html | 80 +++++++------------------------ 1 file changed, 16 insertions(+), 64 deletions(-) diff --git a/webserver/templates/realtime.html b/webserver/templates/realtime.html index 73cb3a5..2f5b712 100644 --- a/webserver/templates/realtime.html +++ b/webserver/templates/realtime.html @@ -76,7 +76,6 @@ border-top: 2px solid #bbb; border-bottom: 2px solid #bbb; margin: -2px 0; - /* Extend clickable area */ transition: background-color 0.2s ease; } @@ -123,7 +122,6 @@ let cmlStats = {}; let selectedCmlId = '10001'; let currentColoringOption = 'completeness'; - let lastTimeRange = null; // Store last known time range from URL // Color functions for each option const colorFunctions = { @@ -374,7 +372,7 @@ } } - // Select CML and update interface + // Select CML and update Grafana dashboard function selectCml(cmlId) { console.log('Selected CML: ' + cmlId); selectedCmlId = cmlId; @@ -382,42 +380,30 @@ var grafanaPanel = document.getElementById('grafana-panel'); try { - // Access the iframe's window + // Update iframe URL via pushState to avoid full reload var iframeWindow = grafanaPanel.contentWindow; var currentUrl = new URL(iframeWindow.location.href); - - // Update only the cml_id parameter currentUrl.searchParams.set('var-cml_id', cmlId); - // Use history.pushState to update URL without reload iframeWindow.history.pushState(null, '', currentUrl.toString()); - - // Dispatch a popstate event to trigger Grafana's variable refresh - var popStateEvent = new PopStateEvent('popstate', { state: null }); - iframeWindow.dispatchEvent(popStateEvent); + iframeWindow.dispatchEvent(new PopStateEvent('popstate', { state: null })); console.log('Updated CML ID via pushState'); } catch (e) { - console.warn('Could not access iframe (cross-origin?), using fallback', e); + // Fallback: rebuild URL with time range preservation + console.warn('Could not access iframe, using fallback', e); - // Fallback: extract time range and rebuild URL - var currentSrc = grafanaPanel.src; var fromParam = null; var toParam = null; try { - var url = new URL(currentSrc); + var url = new URL(grafanaPanel.src); fromParam = url.searchParams.get('from'); toParam = url.searchParams.get('to'); - - if (fromParam && toParam) { - lastTimeRange = { from: fromParam, to: toParam }; - } } catch (e2) { console.warn('Could not parse iframe URL', e2); } - // Build new URL var base = '/grafana/d/cml-realtime/cml-real-time-data'; var params = [ 'orgId=1', @@ -428,47 +414,36 @@ 'kiosk' ]; - if (lastTimeRange && lastTimeRange.from && lastTimeRange.to) { - params.push('from=' + encodeURIComponent(lastTimeRange.from)); - params.push('to=' + encodeURIComponent(lastTimeRange.to)); + if (fromParam && toParam) { + params.push('from=' + encodeURIComponent(fromParam)); + params.push('to=' + encodeURIComponent(toParam)); } grafanaPanel.src = base + '?' + params.join('&'); } - } // Initialize on page load + } + + // Initialize on page load document.addEventListener('DOMContentLoaded', function () { initializeMap(); initializeResizer(); }); - // Initialize resizer for chart height + // Initialize resizer for adjustable dashboard height function initializeResizer() { const resizer = document.getElementById('resizer'); const chartContainer = document.querySelector('.chart-container'); let isResizing = false; - let startY; - let startHeight; - let lastHeight = 400; // Default height - let resizeTimeout; + let startY, startHeight; function startResize(e) { isResizing = true; startY = e.clientY; startHeight = chartContainer.offsetHeight; - lastHeight = startHeight; resizer.classList.add('active'); document.body.style.cursor = 'ns-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); - - // Set up failsafe timeout - if (resizeTimeout) clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => { - if (isResizing) { - console.warn('Resize timeout - forcing stop'); - stopResize(); - } - }, 10000); // 10 second failsafe } function doResize(e) { @@ -476,17 +451,11 @@ const deltaY = startY - e.clientY; const newHeight = startHeight + deltaY; - - // Constrain height between min and max const minHeight = 200; const maxHeight = window.innerHeight * 0.8; const constrainedHeight = Math.max(minHeight, Math.min(maxHeight, newHeight)); - // Only update if the change is significant (reduce sensitivity) - if (Math.abs(constrainedHeight - lastHeight) >= 1) { - chartContainer.style.height = constrainedHeight + 'px'; - lastHeight = constrainedHeight; - } + chartContainer.style.height = constrainedHeight + 'px'; } function stopResize() { @@ -495,31 +464,14 @@ resizer.classList.remove('active'); document.body.style.cursor = ''; document.body.style.userSelect = ''; - // Force reset cursor on html element too - document.documentElement.style.cursor = ''; - // Clear failsafe timeout - if (resizeTimeout) { - clearTimeout(resizeTimeout); - resizeTimeout = null; - } } } - // Event listeners - use capture phase for more reliable event handling + // Use capture phase and window-level events for reliability resizer.addEventListener('mousedown', startResize, true); window.addEventListener('mousemove', doResize, true); window.addEventListener('mouseup', stopResize, true); - - // Additional safety: handle mouse leaving window window.addEventListener('mouseleave', stopResize, true); - - // Failsafe: if resize gets stuck, allow clicking resizer to reset - resizer.addEventListener('click', function (e) { - if (isResizing) { - e.preventDefault(); - stopResize(); - } - }); } {% endblock %} \ No newline at end of file From c30de40c3c30fab2e7b277ed5898906844d05077 Mon Sep 17 00:00:00 2001 From: Christian Chwala Date: Thu, 12 Feb 2026 22:49:38 +0100 Subject: [PATCH 4/4] feat: add resizable Grafana dashboard with overlay layout - Add draggable resizer for dashboard height adjustment (200px-80vh) - Change layout to overlay positioning with map as background - Fix resize dragging by disabling iframe pointer events during resize --- webserver/templates/realtime.html | 126 +++++++++++------------------- 1 file changed, 47 insertions(+), 79 deletions(-) diff --git a/webserver/templates/realtime.html b/webserver/templates/realtime.html index 2f5b712..feb21b2 100644 --- a/webserver/templates/realtime.html +++ b/webserver/templates/realtime.html @@ -25,8 +25,6 @@ max-width: 100% !important; padding: 0 !important; margin: 0 !important; - min-height: calc(100vh - 60px); - width: 100%; } footer { @@ -36,47 +34,46 @@ .realtime-wrapper { width: 100%; min-height: calc(100vh - 60px); - display: flex; - flex-direction: column; position: relative; } .map-container { - flex: 1; - min-height: 0; - position: relative; - width: 100%; - } - - #cml-map { position: absolute; top: 0; left: 0; right: 0; bottom: 0; + z-index: 1; + } + + #cml-map { width: 100%; height: 100%; } .chart-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; height: 400px; - flex-shrink: 0; - position: relative; - width: 100%; min-height: 200px; max-height: 80vh; + z-index: 100; + background: white; } .resizer { - height: 12px; - background: #ddd; + position: absolute; + bottom: 400px; + left: 0; + right: 0; + height: 6px; + background: #ccc; cursor: ns-resize; - position: relative; - z-index: 10; - border-top: 2px solid #bbb; - border-bottom: 2px solid #bbb; - margin: -2px 0; + z-index: 200; transition: background-color 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } .resizer:hover { @@ -88,14 +85,11 @@ } .grafana-container { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + position: relative; border: none; width: 100%; height: 100%; + display: block; } {% endblock %} @@ -374,53 +368,15 @@ // Select CML and update Grafana dashboard function selectCml(cmlId) { - console.log('Selected CML: ' + cmlId); selectedCmlId = cmlId; var grafanaPanel = document.getElementById('grafana-panel'); + var iframeWindow = grafanaPanel.contentWindow; + var currentUrl = new URL(iframeWindow.location.href); + currentUrl.searchParams.set('var-cml_id', cmlId); - try { - // Update iframe URL via pushState to avoid full reload - var iframeWindow = grafanaPanel.contentWindow; - var currentUrl = new URL(iframeWindow.location.href); - currentUrl.searchParams.set('var-cml_id', cmlId); - - iframeWindow.history.pushState(null, '', currentUrl.toString()); - iframeWindow.dispatchEvent(new PopStateEvent('popstate', { state: null })); - - console.log('Updated CML ID via pushState'); - } catch (e) { - // Fallback: rebuild URL with time range preservation - console.warn('Could not access iframe, using fallback', e); - - var fromParam = null; - var toParam = null; - - try { - var url = new URL(grafanaPanel.src); - fromParam = url.searchParams.get('from'); - toParam = url.searchParams.get('to'); - } catch (e2) { - console.warn('Could not parse iframe URL', e2); - } - - var base = '/grafana/d/cml-realtime/cml-real-time-data'; - var params = [ - 'orgId=1', - 'var-cml_id=' + encodeURIComponent(cmlId), - 'refresh=10s', - 'theme=light', - 'viewPanel=2', - 'kiosk' - ]; - - if (fromParam && toParam) { - params.push('from=' + encodeURIComponent(fromParam)); - params.push('to=' + encodeURIComponent(toParam)); - } - - grafanaPanel.src = base + '?' + params.join('&'); - } + iframeWindow.history.pushState(null, '', currentUrl.toString()); + iframeWindow.dispatchEvent(new PopStateEvent('popstate', { state: null })); } // Initialize on page load @@ -443,11 +399,19 @@ resizer.classList.add('active'); document.body.style.cursor = 'ns-resize'; document.body.style.userSelect = 'none'; + + // Prevent iframe from capturing mouse events + const iframe = document.querySelector('.grafana-container'); + if (iframe) { + iframe.style.pointerEvents = 'none'; + } + e.preventDefault(); } function doResize(e) { if (!isResizing) return; + e.preventDefault(); const deltaY = startY - e.clientY; const newHeight = startHeight + deltaY; @@ -456,22 +420,26 @@ const constrainedHeight = Math.max(minHeight, Math.min(maxHeight, newHeight)); chartContainer.style.height = constrainedHeight + 'px'; + resizer.style.bottom = constrainedHeight + 'px'; } function stopResize() { - if (isResizing) { - isResizing = false; - resizer.classList.remove('active'); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; + if (!isResizing) return; + isResizing = false; + resizer.classList.remove('active'); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Re-enable iframe interactions + const iframe = document.querySelector('.grafana-container'); + if (iframe) { + iframe.style.pointerEvents = 'auto'; } } - // Use capture phase and window-level events for reliability - resizer.addEventListener('mousedown', startResize, true); - window.addEventListener('mousemove', doResize, true); - window.addEventListener('mouseup', stopResize, true); - window.addEventListener('mouseleave', stopResize, true); + resizer.addEventListener('mousedown', startResize); + document.addEventListener('mousemove', doResize); + document.addEventListener('mouseup', stopResize); } {% endblock %} \ No newline at end of file