Add real-time system performance charts to dashboard
Backend: GET /api/v1/system/performance endpoint using psutil (CPU/RAM) and nvidia-ml-py (GPU utilization, memory, temperature) with graceful fallback. Frontend: Chart.js line charts with rolling 60-sample history persisted to sessionStorage, flicker-free updates via persistent DOM and diff-based dynamic section refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||
|
||||
@@ -82,61 +83,76 @@ export async function loadDashboard() {
|
||||
const devicesMap = {};
|
||||
for (const d of (devicesData.devices || [])) { devicesMap[d.id] = d; }
|
||||
|
||||
// Build dynamic HTML (targets, profiles)
|
||||
let dynamicHtml = '';
|
||||
|
||||
if (targets.length === 0 && profiles.length === 0) {
|
||||
container.innerHTML = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
||||
return;
|
||||
}
|
||||
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
||||
} else {
|
||||
const enriched = await Promise.all(targets.map(async (target) => {
|
||||
try {
|
||||
const [stateResp, metricsResp] = await Promise.all([
|
||||
fetch(`${API_BASE}/picture-targets/${target.id}/state`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/picture-targets/${target.id}/metrics`, { headers: getHeaders() }),
|
||||
]);
|
||||
const state = stateResp.ok ? await stateResp.json() : {};
|
||||
const metrics = metricsResp.ok ? await metricsResp.json() : {};
|
||||
return { ...target, state, metrics };
|
||||
} catch {
|
||||
return target;
|
||||
}
|
||||
}));
|
||||
|
||||
const enriched = await Promise.all(targets.map(async (target) => {
|
||||
try {
|
||||
const [stateResp, metricsResp] = await Promise.all([
|
||||
fetch(`${API_BASE}/picture-targets/${target.id}/state`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/picture-targets/${target.id}/metrics`, { headers: getHeaders() }),
|
||||
]);
|
||||
const state = stateResp.ok ? await stateResp.json() : {};
|
||||
const metrics = metricsResp.ok ? await metricsResp.json() : {};
|
||||
return { ...target, state, metrics };
|
||||
} catch {
|
||||
return target;
|
||||
const running = enriched.filter(t => t.state && t.state.processing);
|
||||
const stopped = enriched.filter(t => !t.state || !t.state.processing);
|
||||
|
||||
if (profiles.length > 0) {
|
||||
const activeProfiles = profiles.filter(p => p.is_active);
|
||||
const inactiveProfiles = profiles.filter(p => !p.is_active);
|
||||
const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p)).join('');
|
||||
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('profiles', t('dashboard.section.profiles'), profiles.length)}
|
||||
${_sectionContent('profiles', profileItems)}
|
||||
</div>`;
|
||||
}
|
||||
}));
|
||||
|
||||
const running = enriched.filter(t => t.state && t.state.processing);
|
||||
const stopped = enriched.filter(t => !t.state || !t.state.processing);
|
||||
if (running.length > 0) {
|
||||
const stopAllBtn = `<button class="btn btn-sm btn-danger dashboard-stop-all" onclick="event.stopPropagation(); dashboardStopAll()" title="${t('dashboard.stop_all')}">⏹️ ${t('dashboard.stop_all')}</button>`;
|
||||
const runningItems = running.map(target => renderDashboardTarget(target, true, devicesMap)).join('');
|
||||
|
||||
let html = '';
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('running', t('dashboard.section.running'), running.length, stopAllBtn)}
|
||||
${_sectionContent('running', runningItems)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (profiles.length > 0) {
|
||||
const activeProfiles = profiles.filter(p => p.is_active);
|
||||
const inactiveProfiles = profiles.filter(p => !p.is_active);
|
||||
const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p)).join('');
|
||||
if (stopped.length > 0) {
|
||||
const stoppedItems = stopped.map(target => renderDashboardTarget(target, false, devicesMap)).join('');
|
||||
|
||||
html += `<div class="dashboard-section">
|
||||
${_sectionHeader('profiles', t('dashboard.section.profiles'), profiles.length)}
|
||||
${_sectionContent('profiles', profileItems)}
|
||||
</div>`;
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('stopped', t('dashboard.section.stopped'), stopped.length)}
|
||||
${_sectionContent('stopped', stoppedItems)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (running.length > 0) {
|
||||
const stopAllBtn = `<button class="btn btn-sm btn-danger dashboard-stop-all" onclick="event.stopPropagation(); dashboardStopAll()" title="${t('dashboard.stop_all')}">⏹️ ${t('dashboard.stop_all')}</button>`;
|
||||
const runningItems = running.map(target => renderDashboardTarget(target, true, devicesMap)).join('');
|
||||
|
||||
html += `<div class="dashboard-section">
|
||||
${_sectionHeader('running', t('dashboard.section.running'), running.length, stopAllBtn)}
|
||||
${_sectionContent('running', runningItems)}
|
||||
</div>`;
|
||||
// First load: build everything in one innerHTML to avoid flicker
|
||||
const isFirstLoad = !container.querySelector('.dashboard-perf-persistent');
|
||||
if (isFirstLoad) {
|
||||
container.innerHTML = `<div class="dashboard-perf-persistent dashboard-section">
|
||||
${_sectionHeader('perf', t('dashboard.section.performance'), '')}
|
||||
${_sectionContent('perf', renderPerfSection())}
|
||||
</div>
|
||||
<div class="dashboard-dynamic">${dynamicHtml}</div>`;
|
||||
initPerfCharts();
|
||||
} else {
|
||||
const dynamic = container.querySelector('.dashboard-dynamic');
|
||||
if (dynamic.innerHTML !== dynamicHtml) {
|
||||
dynamic.innerHTML = dynamicHtml;
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped.length > 0) {
|
||||
const stoppedItems = stopped.map(target => renderDashboardTarget(target, false, devicesMap)).join('');
|
||||
|
||||
html += `<div class="dashboard-section">
|
||||
${_sectionHeader('stopped', t('dashboard.section.stopped'), stopped.length)}
|
||||
${_sectionContent('stopped', stoppedItems)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
startPerfPolling();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error);
|
||||
|
||||
Reference in New Issue
Block a user