diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index aa879e8..b6d5710 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -35,9 +35,9 @@ import { updateSettingsBaudFpsHint, } from './features/devices.js'; import { - loadDashboard, startDashboardWS, stopDashboardWS, + loadDashboard, startDashboardWS, stopDashboardWS, stopUptimeTimer, dashboardToggleProfile, dashboardStartTarget, dashboardStopTarget, dashboardStopAll, - toggleDashboardSection, + toggleDashboardSection, changeDashboardPollInterval, } from './features/dashboard.js'; import { startPerfPolling, stopPerfPolling, @@ -157,6 +157,8 @@ Object.assign(window, { dashboardStopTarget, dashboardStopAll, toggleDashboardSection, + changeDashboardPollInterval, + stopUptimeTimer, startPerfPolling, stopPerfPolling, diff --git a/server/src/wled_controller/static/js/core/state.js b/server/src/wled_controller/static/js/core/state.js index 17301bd..0d50c79 100644 --- a/server/src/wled_controller/static/js/core/state.js +++ b/server/src/wled_controller/static/js/core/state.js @@ -121,6 +121,15 @@ export function setConfirmResolve(v) { confirmResolve = v; } export let _dashboardLoading = false; export function set_dashboardLoading(v) { _dashboardLoading = v; } +// Dashboard poll interval (ms), persisted in localStorage +const _POLL_KEY = 'dashboard_poll_interval'; +const _POLL_DEFAULT = 2000; +export let dashboardPollInterval = parseInt(localStorage.getItem(_POLL_KEY), 10) || _POLL_DEFAULT; +export function setDashboardPollInterval(v) { + dashboardPollInterval = v; + localStorage.setItem(_POLL_KEY, String(v)); +} + // Pattern template editor state export let patternEditorRects = []; export function setPatternEditorRects(v) { patternEditorRects = v; } diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index 4af2723..b9d1199 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -2,14 +2,192 @@ * Dashboard — real-time target status overview. */ -import { apiKey, _dashboardWS, set_dashboardWS, _dashboardLoading, set_dashboardLoading } from '../core/state.js'; +import { apiKey, _dashboardWS, set_dashboardWS, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js'; import { t } from '../core/i18n.js'; import { escapeHtml, handle401Error } from '../core/api.js'; import { showToast } from '../core/ui.js'; import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js'; +import { startAutoRefresh } from './tabs.js'; const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed'; +const FPS_HISTORY_KEY = 'dashboard_fps_history'; +const MAX_FPS_SAMPLES = 30; + +let _fpsHistory = _loadFpsHistory(); // { targetId: number[] } +let _fpsCharts = {}; // { targetId: Chart } +let _lastRunningIds = []; // sorted target IDs from previous render +let _uptimeBase = {}; // { targetId: { seconds, timestamp } } +let _uptimeTimer = null; + +function _loadFpsHistory() { + try { + const raw = sessionStorage.getItem(FPS_HISTORY_KEY); + if (raw) return JSON.parse(raw); + } catch {} + return {}; +} + +function _saveFpsHistory() { + try { sessionStorage.setItem(FPS_HISTORY_KEY, JSON.stringify(_fpsHistory)); } + catch {} +} + +function _pushFps(targetId, value) { + if (!_fpsHistory[targetId]) _fpsHistory[targetId] = []; + _fpsHistory[targetId].push(value); + if (_fpsHistory[targetId].length > MAX_FPS_SAMPLES) _fpsHistory[targetId].shift(); +} + +function _setUptimeBase(targetId, seconds) { + _uptimeBase[targetId] = { seconds, timestamp: Date.now() }; +} + +function _getInterpolatedUptime(targetId) { + const base = _uptimeBase[targetId]; + if (!base) return null; + const elapsed = (Date.now() - base.timestamp) / 1000; + return base.seconds + elapsed; +} + +function _startUptimeTimer() { + if (_uptimeTimer) return; + _uptimeTimer = setInterval(() => { + for (const id of _lastRunningIds) { + const el = document.querySelector(`[data-uptime-text="${id}"]`); + if (!el) continue; + const seconds = _getInterpolatedUptime(id); + if (seconds != null) { + el.textContent = `🕐 ${formatUptime(seconds)}`; + } + } + }, 1000); +} + +function _stopUptimeTimer() { + if (_uptimeTimer) { + clearInterval(_uptimeTimer); + _uptimeTimer = null; + } + _uptimeBase = {}; +} + +function _destroyFpsCharts() { + for (const id of Object.keys(_fpsCharts)) { + if (_fpsCharts[id]) { _fpsCharts[id].destroy(); } + } + _fpsCharts = {}; +} + +function _createFpsChart(canvasId, history, fpsTarget) { + const canvas = document.getElementById(canvasId); + if (!canvas) return null; + return new Chart(canvas, { + type: 'line', + data: { + labels: history.map(() => ''), + datasets: [{ + data: [...history], + borderColor: '#2196F3', + backgroundColor: 'rgba(33,150,243,0.12)', + borderWidth: 1.5, + tension: 0.3, + fill: true, + pointRadius: 0, + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: true, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { min: 0, max: fpsTarget * 1.15, display: false }, + }, + layout: { padding: 0 }, + }, + }); +} + +function _initFpsCharts(runningTargetIds) { + _destroyFpsCharts(); + // Clean up history for targets that are no longer running + for (const id of Object.keys(_fpsHistory)) { + if (!runningTargetIds.includes(id)) delete _fpsHistory[id]; + } + for (const id of runningTargetIds) { + const canvas = document.getElementById(`dashboard-fps-${id}`); + if (!canvas) continue; + const history = _fpsHistory[id] || []; + const fpsTarget = parseFloat(canvas.dataset.fpsTarget) || 30; + _fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget); + } + _saveFpsHistory(); +} + +/** Update running target metrics in-place (no HTML rebuild). */ +function _updateRunningMetrics(enrichedRunning) { + for (const target of enrichedRunning) { + const state = target.state || {}; + const metrics = target.metrics || {}; + const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-'; + const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-'; + const errors = metrics.errors_count || 0; + + // Push FPS and update chart + if (state.fps_actual != null) { + _pushFps(target.id, state.fps_actual); + } + const chart = _fpsCharts[target.id]; + if (chart) { + const history = _fpsHistory[target.id] || []; + chart.data.datasets[0].data = [...history]; + chart.data.labels = history.map(() => ''); + chart.update(); + } + + // Refresh uptime base for interpolation + if (metrics.uptime_seconds != null) { + _setUptimeBase(target.id, metrics.uptime_seconds); + } + + // Update text values + const fpsEl = document.querySelector(`[data-fps-text="${target.id}"]`); + if (fpsEl) fpsEl.innerHTML = `${fpsActual}/${fpsTarget}`; + + const errorsEl = 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}"]`); + if (row) { + const dot = row.querySelector('.health-dot'); + if (dot && state.device_last_checked != null) { + dot.className = `health-dot ${state.device_online ? 'health-online' : 'health-offline'}`; + } + } + } + } + _saveFpsHistory(); +} + +function _renderPollIntervalSelect() { + const sec = Math.round(dashboardPollInterval / 1000); + return `${sec}s`; +} + +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`; +} function _getCollapsedSections() { try { return JSON.parse(localStorage.getItem(DASHBOARD_COLLAPSED_KEY)) || {}; } @@ -85,6 +263,7 @@ export async function loadDashboard() { // Build dynamic HTML (targets, profiles) let dynamicHtml = ''; + let runningIds = []; if (targets.length === 0 && profiles.length === 0) { dynamicHtml = `