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:
@@ -19,12 +19,15 @@ import { createColorStripCard } from './color-strips.js';
|
||||
// createPatternTemplateCard is imported via window.* to avoid circular deps
|
||||
// (pattern-templates.js calls window.loadTargetsTab)
|
||||
|
||||
// Re-render targets tab when language changes
|
||||
document.addEventListener('languageChanged', () => { if (apiKey) loadTargetsTab(); });
|
||||
// Re-render targets tab when language changes (only if tab is active)
|
||||
document.addEventListener('languageChanged', () => {
|
||||
if (apiKey && localStorage.getItem('activeTab') === 'targets') loadTargetsTab();
|
||||
});
|
||||
|
||||
// --- FPS sparkline history for target cards ---
|
||||
// --- FPS sparkline history and chart instances for target cards ---
|
||||
const _TARGET_MAX_FPS_SAMPLES = 30;
|
||||
const _targetFpsHistory = {};
|
||||
const _targetFpsCharts = {};
|
||||
|
||||
function _pushTargetFps(targetId, value) {
|
||||
if (!_targetFpsHistory[targetId]) _targetFpsHistory[targetId] = [];
|
||||
@@ -339,38 +342,31 @@ export async function loadTargetsTab() {
|
||||
patternTemplates.forEach(pt => { patternTemplateMap[pt.id] = pt; });
|
||||
}
|
||||
|
||||
// Fetch state for each device
|
||||
const devicesWithState = await Promise.all(
|
||||
devices.map(async (device) => {
|
||||
try {
|
||||
const stateResp = await fetch(`${API_BASE}/devices/${device.id}/state`, { headers: getHeaders() });
|
||||
const state = stateResp.ok ? await stateResp.json() : {};
|
||||
return { ...device, state };
|
||||
} catch {
|
||||
return device;
|
||||
}
|
||||
})
|
||||
);
|
||||
// Fetch all device states, target states, and target metrics in batch
|
||||
const [batchDevStatesResp, batchTgtStatesResp, batchTgtMetricsResp] = await Promise.all([
|
||||
fetchWithAuth('/devices/batch/states'),
|
||||
fetchWithAuth('/picture-targets/batch/states'),
|
||||
fetchWithAuth('/picture-targets/batch/metrics'),
|
||||
]);
|
||||
const allDeviceStates = batchDevStatesResp.ok ? (await batchDevStatesResp.json()).states : {};
|
||||
const allTargetStates = batchTgtStatesResp.ok ? (await batchTgtStatesResp.json()).states : {};
|
||||
const allTargetMetrics = batchTgtMetricsResp.ok ? (await batchTgtMetricsResp.json()).metrics : {};
|
||||
|
||||
// Fetch state + metrics for each target (+ colors for KC targets)
|
||||
const devicesWithState = devices.map(d => ({ ...d, state: allDeviceStates[d.id] || {} }));
|
||||
|
||||
// Enrich targets with state/metrics; fetch colors only for running KC targets
|
||||
const targetsWithState = await Promise.all(
|
||||
targets.map(async (target) => {
|
||||
try {
|
||||
const stateResp = await fetch(`${API_BASE}/picture-targets/${target.id}/state`, { headers: getHeaders() });
|
||||
const state = stateResp.ok ? await stateResp.json() : {};
|
||||
const metricsResp = await fetch(`${API_BASE}/picture-targets/${target.id}/metrics`, { headers: getHeaders() });
|
||||
const metrics = metricsResp.ok ? await metricsResp.json() : {};
|
||||
let latestColors = null;
|
||||
if (target.target_type === 'key_colors' && state.processing) {
|
||||
try {
|
||||
const colorsResp = await fetch(`${API_BASE}/picture-targets/${target.id}/colors`, { headers: getHeaders() });
|
||||
if (colorsResp.ok) latestColors = await colorsResp.json();
|
||||
} catch {}
|
||||
}
|
||||
return { ...target, state, metrics, latestColors };
|
||||
} catch {
|
||||
return target;
|
||||
const state = allTargetStates[target.id] || {};
|
||||
const metrics = allTargetMetrics[target.id] || {};
|
||||
let latestColors = null;
|
||||
if (target.target_type === 'key_colors' && state.processing) {
|
||||
try {
|
||||
const colorsResp = await fetch(`${API_BASE}/picture-targets/${target.id}/colors`, { headers: getHeaders() });
|
||||
if (colorsResp.ok) latestColors = await colorsResp.json();
|
||||
} catch {}
|
||||
}
|
||||
return { ...target, state, metrics, latestColors };
|
||||
})
|
||||
);
|
||||
|
||||
@@ -492,6 +488,12 @@ export async function loadTargetsTab() {
|
||||
if (!processingKCIds.has(id)) disconnectKCWebSocket(id);
|
||||
});
|
||||
|
||||
// Destroy old chart instances before DOM rebuild replaces canvases
|
||||
for (const id of Object.keys(_targetFpsCharts)) {
|
||||
_targetFpsCharts[id].destroy();
|
||||
delete _targetFpsCharts[id];
|
||||
}
|
||||
|
||||
// FPS sparkline charts: push samples and init charts after HTML rebuild
|
||||
const allTargets = [...ledTargets, ...kcTargets];
|
||||
const runningIds = new Set();
|
||||
@@ -505,10 +507,11 @@ export async function loadTargetsTab() {
|
||||
const fpsTarget = target.state.fps_target || 30;
|
||||
const device = devices.find(d => d.id === target.device_id);
|
||||
const maxHwFps = device ? _computeMaxFps(device.baud_rate, device.led_count, device.device_type) : null;
|
||||
_createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget, maxHwFps);
|
||||
const chart = _createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget, maxHwFps);
|
||||
if (chart) _targetFpsCharts[target.id] = chart;
|
||||
}
|
||||
});
|
||||
// Clean up history for targets no longer running
|
||||
// Clean up history and charts for targets no longer running
|
||||
Object.keys(_targetFpsHistory).forEach(id => {
|
||||
if (!runningIds.has(id)) delete _targetFpsHistory[id];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user