/** * Performance charts — real-time CPU, RAM, GPU usage with Chart.js. * History is seeded from the server-side ring buffer on init. */ import { API_BASE, getHeaders } from '../core/api.js'; import { t } from '../core/i18n.js'; import { dashboardPollInterval } from '../core/state.js'; import { createColorPicker, registerColorPicker } from '../core/color-picker.js'; const MAX_SAMPLES = 120; const CHART_KEYS = ['cpu', 'ram', 'gpu']; let _pollTimer = null; let _charts = {}; // { cpu: Chart, ram: Chart, gpu: Chart } let _history = { cpu: [], ram: [], gpu: [] }; let _hasGpu = null; // null = unknown, true/false after first fetch function _getColor(key) { return localStorage.getItem(`perfChartColor_${key}`) || getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#4CAF50'; } function _onChartColorChange(key, hex) { if (hex) { localStorage.setItem(`perfChartColor_${key}`, hex); } else { // Reset: remove saved color, fall back to default localStorage.removeItem(`perfChartColor_${key}`); hex = _getColor(key); // Update swatch to show the actual default color const swatch = document.getElementById(`cp-swatch-perf-${key}`); if (swatch) swatch.style.background = hex; } const chart = _charts[key]; if (chart) { chart.data.datasets[0].borderColor = hex; chart.data.datasets[0].backgroundColor = hex + '26'; chart.update(); } } /** Returns the static HTML for the perf section (canvas placeholders). */ export function renderPerfSection() { // Register callbacks before rendering for (const key of CHART_KEYS) { registerColorPicker(`perf-${key}`, hex => _onChartColorChange(key, hex)); } return `
${t('dashboard.perf.cpu')} ${createColorPicker({ id: 'perf-cpu', currentColor: _getColor('cpu'), anchor: 'left', showReset: true })} -
${t('dashboard.perf.ram')} ${createColorPicker({ id: 'perf-ram', currentColor: _getColor('ram'), anchor: 'left', showReset: true })} -
${t('dashboard.perf.gpu')} ${createColorPicker({ id: 'perf-gpu', currentColor: _getColor('gpu'), anchor: 'left', showReset: true })} -
`; } function _createChart(canvasId, key) { const ctx = document.getElementById(canvasId); if (!ctx) return null; const color = _getColor(key); return new Chart(ctx, { type: 'line', data: { labels: Array(MAX_SAMPLES).fill(''), datasets: [{ data: [], borderColor: color, backgroundColor: color + '26', borderWidth: 1.5, tension: 0.3, fill: true, pointRadius: 0, }], }, options: { responsive: true, maintainAspectRatio: false, animation: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { min: 0, max: 100, display: false }, }, layout: { padding: 0 }, }, }); } /** Seed charts from server-side metrics history. */ async function _seedFromServer() { try { const resp = await fetch(`${API_BASE}/system/metrics-history`, { headers: getHeaders() }); if (!resp.ok) return; const data = await resp.json(); const samples = data.system || []; _history.cpu = samples.map(s => s.cpu).filter(v => v != null); _history.ram = samples.map(s => s.ram_pct).filter(v => v != null); _history.gpu = samples.map(s => s.gpu_util).filter(v => v != null); // Detect GPU availability from history if (_history.gpu.length > 0) { _hasGpu = true; } for (const key of CHART_KEYS) { if (_charts[key] && _history[key].length > 0) { _charts[key].data.datasets[0].data = [..._history[key]]; _charts[key].data.labels = _history[key].map(() => ''); _charts[key].update(); } } } catch { // Silently ignore — charts will fill from polling } } /** Initialize Chart.js instances on the already-mounted canvases. */ export async function initPerfCharts() { _destroyCharts(); _charts.cpu = _createChart('perf-chart-cpu', 'cpu'); _charts.ram = _createChart('perf-chart-ram', 'ram'); _charts.gpu = _createChart('perf-chart-gpu', 'gpu'); await _seedFromServer(); } function _destroyCharts() { for (const key of Object.keys(_charts)) { if (_charts[key]) { _charts[key].destroy(); _charts[key] = null; } } } function _pushSample(key, value) { _history[key].push(value); if (_history[key].length > MAX_SAMPLES) _history[key].shift(); const chart = _charts[key]; if (!chart) return; const ds = chart.data.datasets[0].data; ds.length = 0; ds.push(..._history[key]); // Ensure labels array matches length (reuse existing array) while (chart.data.labels.length < ds.length) chart.data.labels.push(''); chart.data.labels.length = ds.length; chart.update('none'); } async function _fetchPerformance() { try { const resp = await fetch(`${API_BASE}/system/performance`, { headers: getHeaders() }); if (!resp.ok) return; const data = await resp.json(); // CPU _pushSample('cpu', data.cpu_percent); const cpuEl = document.getElementById('perf-cpu-value'); if (cpuEl) cpuEl.textContent = `${data.cpu_percent.toFixed(0)}%`; if (data.cpu_name) { const nameEl = document.getElementById('perf-cpu-name'); if (nameEl && !nameEl.textContent) nameEl.textContent = data.cpu_name; } // RAM _pushSample('ram', data.ram_percent); const ramEl = document.getElementById('perf-ram-value'); if (ramEl) { const usedGb = (data.ram_used_mb / 1024).toFixed(1); const totalGb = (data.ram_total_mb / 1024).toFixed(1); ramEl.textContent = `${usedGb}/${totalGb} GB`; } // GPU if (data.gpu) { _hasGpu = true; _pushSample('gpu', data.gpu.utilization); const gpuEl = document.getElementById('perf-gpu-value'); if (gpuEl) gpuEl.textContent = `${data.gpu.utilization.toFixed(0)}% · ${data.gpu.temperature_c}°C`; if (data.gpu.name) { const nameEl = document.getElementById('perf-gpu-name'); if (nameEl && !nameEl.textContent) nameEl.textContent = data.gpu.name; } } else if (_hasGpu === null) { _hasGpu = false; const card = document.getElementById('perf-gpu-card'); if (card) { const canvas = card.querySelector('canvas'); if (canvas) canvas.style.display = 'none'; const noGpu = document.createElement('div'); noGpu.className = 'perf-chart-unavailable'; noGpu.textContent = t('dashboard.perf.unavailable'); card.appendChild(noGpu); } } } catch { // Silently ignore fetch errors (e.g., network issues, tab hidden) } } export function startPerfPolling() { if (_pollTimer) return; _fetchPerformance(); _pollTimer = setInterval(_fetchPerformance, dashboardPollInterval); } export function stopPerfPolling() { if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; } } // Pause polling when browser tab becomes hidden, resume when visible document.addEventListener('visibilitychange', () => { if (document.hidden) { stopPerfPolling(); } else { // Only resume if dashboard is active const activeTab = localStorage.getItem('activeTab') || 'dashboard'; if (activeTab === 'dashboard') startPerfPolling(); } });