Codebase review: stability, performance, usability, and i18n fixes
Stability: - Fix race condition: set _is_running before create_task in target processors - Await probe task after cancel in wled_target_processor - Replace raw fetch() with fetchWithAuth() across devices, kc-targets, pattern-templates - Add try/catch to showTestTemplateModal in streams.js - Wrap blocking I/O in asyncio.to_thread (picture_targets, system restore) - Fix dashboardStopAll to filter only running targets with ok guard Performance: - Vectorize fire effect spark loop with numpy in effect_stream - Vectorize FFT band binning with cumulative sum in analysis.py - Rewrite pixel_processor with vectorized numpy (accept ndarray or list) - Add httpx.AsyncClient connection pooling with lock in wled_provider - Optimize _send_pixels_http to avoid np.hstack allocation in wled_client - Mutate chart arrays in-place in dashboard, perf-charts, targets - Merge dashboard 2-batch fetch into single Promise.all - Hoist frame_time outside loop in mapped_stream Usability: - Fix health check interval load/save in device settings - Swap confirm modal button classes (No=secondary, Yes=danger) - Add aria-modal to audio/value source editors, fix close button aria-labels - Add modal footer close button to settings modal - Add dedicated calibration LED count validation error keys i18n: - Replace ~50 hardcoded English strings with t() calls across 12 JS files - Add 50 new keys to en.json, ru.json, zh.json - Localize inline toasts in index.html with window.t fallback - Add data-i18n to command palette footer - Add localization policy to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -198,10 +198,16 @@ function _updateRunningMetrics(enrichedRunning) {
|
||||
if (chart) {
|
||||
const actualH = _fpsHistory[target.id] || [];
|
||||
const currentH = _fpsCurrentHistory[target.id] || [];
|
||||
chart.data.datasets[0].data = [...actualH];
|
||||
chart.data.datasets[1].data = [...currentH];
|
||||
chart.data.labels = actualH.map(() => '');
|
||||
chart.update();
|
||||
// Mutate in-place to avoid array copies
|
||||
const ds0 = chart.data.datasets[0].data;
|
||||
ds0.length = 0;
|
||||
ds0.push(...actualH);
|
||||
const ds1 = chart.data.datasets[1].data;
|
||||
ds1.length = 0;
|
||||
ds1.push(...currentH);
|
||||
while (chart.data.labels.length < ds0.length) chart.data.labels.push('');
|
||||
chart.data.labels.length = ds0.length;
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
// Refresh uptime base for interpolation
|
||||
@@ -366,11 +372,14 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
setTabRefreshing('dashboard-content', true);
|
||||
|
||||
try {
|
||||
const [targetsResp, profilesResp, devicesResp, cssResp] = await Promise.all([
|
||||
// Fire all requests in a single batch to avoid sequential RTTs
|
||||
const [targetsResp, profilesResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp] = await Promise.all([
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/profiles').catch(() => null),
|
||||
fetchWithAuth('/devices').catch(() => null),
|
||||
fetchWithAuth('/color-strip-sources').catch(() => null),
|
||||
fetchWithAuth('/picture-targets/batch/states').catch(() => null),
|
||||
fetchWithAuth('/picture-targets/batch/metrics').catch(() => null),
|
||||
]);
|
||||
|
||||
const targetsData = await targetsResp.json();
|
||||
@@ -384,6 +393,9 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
const cssSourceMap = {};
|
||||
for (const s of (cssData.sources || [])) { cssSourceMap[s.id] = s; }
|
||||
|
||||
const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
|
||||
const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
|
||||
|
||||
// Build dynamic HTML (targets, profiles)
|
||||
let dynamicHtml = '';
|
||||
let runningIds = [];
|
||||
@@ -392,12 +404,6 @@ 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 [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] || {},
|
||||
@@ -712,7 +718,7 @@ export async function dashboardToggleProfile(profileId, enable) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to toggle profile', 'error');
|
||||
showToast(t('dashboard.error.profile_toggle_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,11 +732,11 @@ export async function dashboardStartTarget(targetId) {
|
||||
loadDashboard();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(`Failed to start: ${error.detail}`, 'error');
|
||||
showToast(t('dashboard.error.start_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to start processing', 'error');
|
||||
showToast(t('dashboard.error.start_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,11 +750,11 @@ export async function dashboardStopTarget(targetId) {
|
||||
loadDashboard();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(`Failed to stop: ${error.detail}`, 'error');
|
||||
showToast(t('dashboard.error.stop_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to stop processing', 'error');
|
||||
showToast(t('dashboard.error.stop_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,26 +769,31 @@ export async function dashboardToggleAutoStart(targetId, enable) {
|
||||
loadDashboard();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(`Failed: ${error.detail}`, 'error');
|
||||
showToast(t('dashboard.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to toggle auto-start', 'error');
|
||||
showToast(t('dashboard.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardStopAll() {
|
||||
try {
|
||||
const targetsResp = await fetchWithAuth('/picture-targets');
|
||||
const [targetsResp, statesResp] = await Promise.all([
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/picture-targets/batch/states'),
|
||||
]);
|
||||
const data = await targetsResp.json();
|
||||
const running = (data.targets || []).filter(t => t.id);
|
||||
const statesData = statesResp.ok ? await statesResp.json() : { states: {} };
|
||||
const states = statesData.states || {};
|
||||
const running = (data.targets || []).filter(t => states[t.id]?.processing);
|
||||
await Promise.all(running.map(t =>
|
||||
fetchWithAuth(`/picture-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {})
|
||||
));
|
||||
loadDashboard();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to stop all targets', 'error');
|
||||
showToast(t('dashboard.error.stop_all'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user