diff --git a/server/src/wled_controller/static/js/features/calibration.js b/server/src/wled_controller/static/js/features/calibration.js index 49144c7..ab3f961 100644 --- a/server/src/wled_controller/static/js/features/calibration.js +++ b/server/src/wled_controller/static/js/features/calibration.js @@ -45,6 +45,9 @@ class CalibrationModal extends Modal { const calibModal = new CalibrationModal(); +let _dragRaf = null; +let _previewRaf = null; + /* ── Public API (exported names unchanged) ────────────────────── */ export async function showCalibration(deviceId) { @@ -117,8 +120,12 @@ export async function showCalibration(deviceId) { if (!window._calibrationResizeObserver) { window._calibrationResizeObserver = new ResizeObserver(() => { - updateSpanBars(); - renderCalibrationCanvas(); + if (window._calibrationResizeRaf) return; + window._calibrationResizeRaf = requestAnimationFrame(() => { + window._calibrationResizeRaf = null; + updateSpanBars(); + renderCalibrationCanvas(); + }); }); } window._calibrationResizeObserver.observe(preview); @@ -202,8 +209,12 @@ export function updateCalibrationPreview() { if (toggleEl) toggleEl.classList.toggle('edge-disabled', count === 0); }); - updateSpanBars(); - renderCalibrationCanvas(); + if (_previewRaf) cancelAnimationFrame(_previewRaf); + _previewRaf = requestAnimationFrame(() => { + _previewRaf = null; + updateSpanBars(); + renderCalibrationCanvas(); + }); } export function renderCalibrationCanvas() { @@ -509,11 +520,19 @@ function initSpanDrag() { if (handleType === 'start') span.start = Math.min(fraction, span.end - MIN_SPAN); else span.end = Math.max(fraction, span.start + MIN_SPAN); - updateSpanBars(); - renderCalibrationCanvas(); + if (!_dragRaf) { + _dragRaf = requestAnimationFrame(() => { + _dragRaf = null; + updateSpanBars(); + renderCalibrationCanvas(); + }); + } } function onMouseUp() { + if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; } + updateSpanBars(); + renderCalibrationCanvas(); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } @@ -548,11 +567,19 @@ function initSpanDrag() { span.start = newStart; span.end = newStart + spanWidth; - updateSpanBars(); - renderCalibrationCanvas(); + if (!_dragRaf) { + _dragRaf = requestAnimationFrame(() => { + _dragRaf = null; + updateSpanBars(); + renderCalibrationCanvas(); + }); + } } function onMouseUp() { + if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; } + updateSpanBars(); + renderCalibrationCanvas(); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index a13dcc6..507d95d 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -19,6 +19,7 @@ let _fpsCharts = {}; // { targetId: Chart } let _lastRunningIds = []; // sorted target IDs from previous render let _uptimeBase = {}; // { targetId: { seconds, timestamp } } let _uptimeTimer = null; +let _metricsElements = new Map(); function _loadFpsHistory() { try { @@ -99,7 +100,7 @@ function _createFpsChart(canvasId, history, fpsTarget) { options: { responsive: true, maintainAspectRatio: false, - animation: true, + animation: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, @@ -124,6 +125,18 @@ function _initFpsCharts(runningTargetIds) { _fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget); } _saveFpsHistory(); + _cacheMetricsElements(runningTargetIds); +} + +function _cacheMetricsElements(runningIds) { + _metricsElements.clear(); + for (const id of runningIds) { + _metricsElements.set(id, { + fps: document.querySelector(`[data-fps-text="${id}"]`), + errors: document.querySelector(`[data-errors-text="${id}"]`), + row: document.querySelector(`[data-target-id="${id}"]`), + }); + } } /** Update running target metrics in-place (no HTML rebuild). */ @@ -152,17 +165,18 @@ function _updateRunningMetrics(enrichedRunning) { _setUptimeBase(target.id, metrics.uptime_seconds); } - // Update text values - const fpsEl = document.querySelector(`[data-fps-text="${target.id}"]`); + // Update text values (use cached refs, fallback to querySelector) + const cached = _metricsElements.get(target.id); + const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`); if (fpsEl) fpsEl.innerHTML = `${fpsActual}/${fpsTarget}`; - const errorsEl = document.querySelector(`[data-errors-text="${target.id}"]`); + const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`); if (errorsEl) errorsEl.textContent = `${errors > 0 ? '⚠️' : '✅'} ${errors}`; // Update health dot const isLed = target.target_type === 'led' || target.target_type === 'wled'; if (isLed) { - const row = document.querySelector(`[data-target-id="${target.id}"]`); + const row = cached?.row || document.querySelector(`[data-target-id="${target.id}"]`); if (row) { const dot = row.querySelector('.health-dot'); if (dot && state.device_last_checked != null) { @@ -174,19 +188,55 @@ function _updateRunningMetrics(enrichedRunning) { _saveFpsHistory(); } +function _updateProfilesInPlace(profiles) { + for (const p of profiles) { + const card = document.querySelector(`[data-profile-id="${p.id}"]`); + if (!card) continue; + const badge = card.querySelector('.dashboard-badge-active, .dashboard-badge-stopped'); + if (badge) { + if (!p.enabled) { + badge.className = 'dashboard-badge-stopped'; + badge.textContent = t('profiles.status.disabled'); + } else if (p.is_active) { + badge.className = 'dashboard-badge-active'; + badge.textContent = t('profiles.status.active'); + } else { + badge.className = 'dashboard-badge-stopped'; + badge.textContent = t('profiles.status.inactive'); + } + } + const metricVal = card.querySelector('.dashboard-metric-value'); + if (metricVal) { + const cnt = p.target_ids.length; + const active = (p.active_target_ids || []).length; + metricVal.textContent = p.is_active ? `${active}/${cnt}` : `${cnt}`; + } + const btn = card.querySelector('.dashboard-target-actions .btn'); + if (btn) { + btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`; + btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`); + btn.textContent = p.enabled ? '⏸' : '▶'; + } + } +} + function _renderPollIntervalSelect() { const sec = Math.round(dashboardPollInterval / 1000); return `${sec}s`; } +let _pollDebounce = null; export function changeDashboardPollInterval(value) { - const ms = parseInt(value, 10) * 1000; - setDashboardPollInterval(ms); - startAutoRefresh(); - stopPerfPolling(); - startPerfPolling(); const label = document.querySelector('.dashboard-poll-value'); if (label) label.textContent = `${value}s`; + clearTimeout(_pollDebounce); + _pollDebounce = setTimeout(() => { + const ms = parseInt(value, 10) * 1000; + setDashboardPollInterval(ms); + startAutoRefresh(); + stopPerfPolling(); + startPerfPolling(); + }, 300); } function _getCollapsedSections() { @@ -289,11 +339,18 @@ export async function loadDashboard(forceFullRender = false) { const newRunningIds = running.map(t => t.id).sort().join(','); const prevRunningIds = [..._lastRunningIds].sort().join(','); const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent'); - if (!forceFullRender && hasExistingDom && newRunningIds === prevRunningIds && newRunningIds !== '') { + const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds; + if (structureUnchanged && !forceFullRender && running.length > 0) { _updateRunningMetrics(running); set_dashboardLoading(false); return; } + if (structureUnchanged && forceFullRender) { + if (running.length > 0) _updateRunningMetrics(running); + _updateProfilesInPlace(profiles); + set_dashboardLoading(false); + return; + } if (profiles.length > 0) { const activeProfiles = profiles.filter(p => p.is_active); @@ -475,7 +532,7 @@ function renderDashboardProfile(profile) { const activeCount = (profile.active_target_ids || []).length; const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`; - return `