Batch API endpoints, reduce frontend polling by ~75%, fix resource leaks

Backend: add batch endpoints for target states, metrics, and device
health to replace O(N) individual API calls per poll cycle.
Frontend: use batch endpoints in dashboard/targets/profiles tabs,
fix Chart.js instance leaks, debounce server event reloads, add
i18n active-tab guards, clean up ResizeObserver on pattern editor
close, cache uptime timer DOM refs, increase KC auto-refresh to 2s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 18:55:09 +03:00
parent d4a0f3a7f5
commit 9392741f08
8 changed files with 125 additions and 73 deletions

View File

@@ -18,6 +18,7 @@ let _fpsCharts = {}; // { targetId: Chart }
let _lastRunningIds = []; // sorted target IDs from previous render
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
let _uptimeTimer = null;
let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs
let _metricsElements = new Map();
function _loadFpsHistory() {
@@ -50,11 +51,19 @@ function _getInterpolatedUptime(targetId) {
return base.seconds + elapsed;
}
function _cacheUptimeElements() {
_uptimeElements = {};
for (const id of _lastRunningIds) {
const el = document.querySelector(`[data-uptime-text="${id}"]`);
if (el) _uptimeElements[id] = el;
}
}
function _startUptimeTimer() {
if (_uptimeTimer) return;
_uptimeTimer = setInterval(() => {
for (const id of _lastRunningIds) {
const el = document.querySelector(`[data-uptime-text="${id}"]`);
const el = _uptimeElements[id];
if (!el) continue;
const seconds = _getInterpolatedUptime(id);
if (seconds != null) {
@@ -70,6 +79,7 @@ function _stopUptimeTimer() {
_uptimeTimer = null;
}
_uptimeBase = {};
_uptimeElements = {};
}
function _destroyFpsCharts() {
@@ -320,18 +330,16 @@ export async function loadDashboard(forceFullRender = false) {
if (targets.length === 0 && profiles.length === 0) {
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 [batchStatesResp, batchMetricsResp] = await Promise.all([
fetchWithAuth('/picture-targets/batch/states'),
fetchWithAuth('/picture-targets/batch/metrics'),
]);
const allStates = batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
const allMetrics = batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
const enriched = targets.map(target => ({
...target,
state: allStates[target.id] || {},
metrics: allMetrics[target.id] || {},
}));
const running = enriched.filter(t => t.state && t.state.processing);
@@ -412,6 +420,7 @@ export async function loadDashboard(forceFullRender = false) {
}
}
_lastRunningIds = runningIds;
_cacheUptimeElements();
_initFpsCharts(runningIds);
_startUptimeTimer();
startPerfPolling();
@@ -637,13 +646,15 @@ function _isDashboardActive() {
return (localStorage.getItem('activeTab') || 'dashboard') === 'dashboard';
}
document.addEventListener('server:state_change', () => {
if (_isDashboardActive()) loadDashboard();
});
let _eventDebounceTimer = null;
function _debouncedDashboardReload(forceFullRender = false) {
if (!_isDashboardActive()) return;
clearTimeout(_eventDebounceTimer);
_eventDebounceTimer = setTimeout(() => loadDashboard(forceFullRender), 300);
}
document.addEventListener('server:profile_state_changed', () => {
if (_isDashboardActive()) loadDashboard(true);
});
document.addEventListener('server:state_change', () => _debouncedDashboardReload());
document.addEventListener('server:profile_state_changed', () => _debouncedDashboardReload(true));
// Re-render dashboard when language changes
document.addEventListener('languageChanged', () => {