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:
2026-02-28 12:12:37 +03:00
parent c95c6e9a44
commit bd8d7a019f
31 changed files with 460 additions and 233 deletions

View File

@@ -148,10 +148,11 @@ export async function saveAudioSource() {
export async function editAudioSource(sourceId) {
try {
const resp = await fetchWithAuth(`/audio-sources/${sourceId}`);
if (!resp.ok) throw new Error('fetch failed');
if (!resp.ok) throw new Error(t('audio_source.error.load'));
const data = await resp.json();
await showAudioSourceModal(data.source_type, data);
} catch (e) {
if (e.isAuth) return;
showToast(e.message, 'error');
}
}
@@ -161,7 +162,7 @@ export async function editAudioSource(sourceId) {
export async function cloneAudioSource(sourceId) {
try {
const resp = await fetchWithAuth(`/audio-sources/${sourceId}`);
if (!resp.ok) throw new Error('fetch failed');
if (!resp.ok) throw new Error(t('audio_source.error.load'));
const data = await resp.json();
delete data.id;
data.name = data.name + ' (copy)';

View File

@@ -6,6 +6,7 @@ import {
calibrationTestState, EDGE_TEST_COLORS,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
import { t } from '../core/i18n.js';
import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { closeTutorial, startCalibrationTutorial } from './tutorials.js';
@@ -138,7 +139,7 @@ export async function showCalibration(deviceId) {
fetchWithAuth('/config/displays'),
]);
if (!response.ok) { showToast('Failed to load calibration', 'error'); return; }
if (!response.ok) { showToast(t('calibration.error.load_failed'), 'error'); return; }
const device = await response.json();
const calibration = device.calibration;
@@ -215,7 +216,7 @@ export async function showCalibration(deviceId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to load calibration:', error);
showToast('Failed to load calibration', 'error');
showToast(t('calibration.error.load_failed'), 'error');
}
}
@@ -240,7 +241,7 @@ export async function showCSSCalibration(cssId) {
fetchWithAuth('/devices'),
]);
if (!cssResp.ok) { showToast('Failed to load color strip source', 'error'); return; }
if (!cssResp.ok) { showToast(t('calibration.error.css_load_failed'), 'error'); return; }
const source = await cssResp.json();
const calibration = source.calibration || {};
@@ -339,7 +340,7 @@ export async function showCSSCalibration(cssId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to load CSS calibration:', error);
showToast('Failed to load calibration', 'error');
showToast(t('calibration.error.load_failed'), 'error');
}
}
@@ -841,13 +842,13 @@ export async function toggleTestEdge(edge) {
});
if (!response.ok) {
const errorData = await response.json();
error.textContent = `Test failed: ${errorData.detail}`;
error.textContent = t('calibration.error.test_toggle_failed');
error.style.display = 'block';
}
} catch (err) {
if (err.isAuth) return;
console.error('Failed to toggle CSS test edge:', err);
error.textContent = 'Failed to toggle test edge';
error.textContent = t('calibration.error.test_toggle_failed');
error.style.display = 'block';
}
return;
@@ -871,13 +872,13 @@ export async function toggleTestEdge(edge) {
});
if (!response.ok) {
const errorData = await response.json();
error.textContent = `Test failed: ${errorData.detail}`;
error.textContent = t('calibration.error.test_toggle_failed');
error.style.display = 'block';
}
} catch (err) {
if (err.isAuth) return;
console.error('Failed to toggle test edge:', err);
error.textContent = 'Failed to toggle test edge';
error.textContent = t('calibration.error.test_toggle_failed');
error.style.display = 'block';
}
}
@@ -920,13 +921,13 @@ export async function saveCalibration() {
: parseInt(document.getElementById('cal-device-led-count-inline').textContent) || 0;
if (!cssMode) {
if (total !== declaredLedCount) {
error.textContent = `Total LEDs (${total}) must equal device LED count (${declaredLedCount})`;
error.textContent = t('calibration.error.led_count_mismatch');
error.style.display = 'block';
return;
}
} else {
if (declaredLedCount > 0 && total > declaredLedCount) {
error.textContent = `Calibrated LEDs (${total}) exceed total LED count (${declaredLedCount})`;
error.textContent = t('calibration.error.led_count_exceeded');
error.style.display = 'block';
return;
}
@@ -963,7 +964,7 @@ export async function saveCalibration() {
});
}
if (response.ok) {
showToast('Calibration saved', 'success');
showToast(t('calibration.saved'), 'success');
calibModal.forceClose();
if (cssMode) {
if (window.loadTargetsTab) window.loadTargetsTab();
@@ -972,13 +973,13 @@ export async function saveCalibration() {
}
} else {
const errorData = await response.json();
error.textContent = `Failed to save: ${errorData.detail}`;
error.textContent = t('calibration.error.save_failed');
error.style.display = 'block';
}
} catch (err) {
if (err.isAuth) return;
console.error('Failed to save calibration:', err);
error.textContent = 'Failed to save calibration';
error.textContent = t('calibration.error.save_failed');
error.style.display = 'block';
}
}

View File

@@ -525,7 +525,7 @@ async function _loadAudioSources() {
if (!select) return;
try {
const resp = await fetchWithAuth('/audio-sources');
if (!resp.ok) throw new Error('fetch failed');
if (!resp.ok) return;
const data = await resp.json();
const sources = data.sources || [];
select.innerHTML = sources.map(s => {
@@ -905,7 +905,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to open CSS editor:', error);
showToast('Failed to open color strip editor', 'error');
showToast(t('color_strip.error.editor_open_failed'), 'error');
}
}
@@ -1097,7 +1097,7 @@ export async function cloneColorStrip(cssId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone color strip:', error);
showToast('Failed to clone color strip source', 'error');
showToast(t('color_strip.error.clone_failed'), 'error');
}
}
@@ -1122,7 +1122,7 @@ export async function deleteColorStrip(cssId) {
}
} catch (error) {
if (error.isAuth) return;
showToast('Failed to delete color strip source', 'error');
showToast(t('color_strip.error.delete_failed'), 'error');
}
}

View File

@@ -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');
}
}

View File

@@ -317,7 +317,7 @@ export async function handleAddDevice(event) {
}
if (!name || (!isMockDevice(deviceType) && !url)) {
error.textContent = 'Please fill in all fields';
error.textContent = t('device_discovery.error.fill_all_fields');
error.style.display = 'block';
return;
}
@@ -351,7 +351,7 @@ export async function handleAddDevice(event) {
if (response.ok) {
const result = await response.json();
console.log('Device added successfully:', result);
showToast('Device added successfully', 'success');
showToast(t('device_discovery.added'), 'success');
addDeviceModal.forceClose();
// Use window.* to avoid circular imports
if (typeof window.loadDevices === 'function') await window.loadDevices();
@@ -365,12 +365,12 @@ export async function handleAddDevice(event) {
} else {
const errorData = await response.json();
console.error('Failed to add device:', errorData);
error.textContent = `Failed to add device: ${errorData.detail}`;
error.textContent = t('device_discovery.error.add_failed');
error.style.display = 'block';
}
} catch (err) {
if (err.isAuth) return;
console.error('Failed to add device:', err);
showToast('Failed to add device', 'error');
showToast(t('device_discovery.error.add_failed'), 'error');
}
}

View File

@@ -126,7 +126,7 @@ export async function turnOffDevice(deviceId) {
}
} catch (error) {
if (error.isAuth) return;
showToast('Failed to turn off device', 'error');
showToast(t('device.error.power_off_failed'), 'error');
}
}
@@ -143,23 +143,23 @@ export async function removeDevice(deviceId) {
method: 'DELETE',
});
if (response.ok) {
showToast('Device removed', 'success');
showToast(t('device.removed'), 'success');
window.loadDevices();
} else {
const error = await response.json();
showToast(`Failed to remove: ${error.detail}`, 'error');
showToast(t('device.error.remove_failed'), 'error');
}
} catch (error) {
if (error.isAuth) return;
console.error('Failed to remove device:', error);
showToast('Failed to remove device', 'error');
showToast(t('device.error.remove_failed'), 'error');
}
}
export async function showSettings(deviceId) {
try {
const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`);
if (!deviceResponse.ok) { showToast('Failed to load device settings', 'error'); return; }
if (!deviceResponse.ok) { showToast(t('device.error.settings_load_failed'), 'error'); return; }
const device = await deviceResponse.json();
const isAdalight = isSerialDevice(device.device_type);
@@ -171,7 +171,7 @@ export async function showSettings(deviceId) {
document.getElementById('settings-device-id').value = device.id;
document.getElementById('settings-device-name').value = device.name;
document.getElementById('settings-health-interval').value = 30;
document.getElementById('settings-health-interval').value = device.state_check_interval ?? 30;
const isMock = isMockDevice(device.device_type);
const urlGroup = document.getElementById('settings-url-group');
@@ -242,7 +242,7 @@ export async function showSettings(deviceId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to load device settings:', error);
showToast('Failed to load device settings', 'error');
showToast(t('device.error.settings_load_failed'), 'error');
}
}
@@ -256,12 +256,16 @@ export async function saveDeviceSettings() {
const url = settingsModal._getUrl();
if (!name || !url) {
settingsModal.showError('Please fill in all fields correctly');
settingsModal.showError(t('device.error.required'));
return;
}
try {
const body = { name, url, auto_shutdown: document.getElementById('settings-auto-shutdown').checked };
const body = {
name, url,
auto_shutdown: document.getElementById('settings-auto-shutdown').checked,
state_check_interval: parseInt(document.getElementById('settings-health-interval').value, 10) || 30,
};
const ledCountInput = document.getElementById('settings-led-count');
if (settingsModal.capabilities.includes('manual_led_count') && ledCountInput.value) {
body.led_count = parseInt(ledCountInput.value, 10);
@@ -283,7 +287,7 @@ export async function saveDeviceSettings() {
if (!deviceResponse.ok) {
const errorData = await deviceResponse.json();
settingsModal.showError(`Failed to update device: ${errorData.detail}`);
settingsModal.showError(t('device.error.update'));
return;
}
@@ -293,7 +297,7 @@ export async function saveDeviceSettings() {
} catch (err) {
if (err.isAuth) return;
console.error('Failed to save device settings:', err);
settingsModal.showError('Failed to save settings');
settingsModal.showError(t('device.error.save'));
}
}
@@ -307,14 +311,16 @@ export async function saveCardBrightness(deviceId, value) {
const bri = parseInt(value);
updateDeviceBrightness(deviceId, bri);
try {
await fetch(`${API_BASE}/devices/${deviceId}/brightness`, {
const resp = await fetchWithAuth(`/devices/${deviceId}/brightness`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ brightness: bri })
});
if (!resp.ok) {
showToast(t('device.error.brightness'), 'error');
}
} catch (err) {
console.error('Failed to update brightness:', err);
showToast('Failed to update brightness', 'error');
if (err.isAuth) return;
showToast(t('device.error.brightness'), 'error');
}
}
@@ -323,9 +329,7 @@ export async function fetchDeviceBrightness(deviceId) {
if (_brightnessFetchInFlight.has(deviceId)) return;
_brightnessFetchInFlight.add(deviceId);
try {
const resp = await fetch(`${API_BASE}/devices/${deviceId}/brightness`, {
headers: getHeaders()
});
const resp = await fetchWithAuth(`/devices/${deviceId}/brightness`);
if (!resp.ok) return;
const data = await resp.json();
updateDeviceBrightness(deviceId, data.brightness);
@@ -398,9 +402,7 @@ async function _populateSettingsSerialPorts(currentUrl) {
try {
const discoverType = settingsModal.deviceType || 'adalight';
const resp = await fetch(`${API_BASE}/devices/discover?timeout=2&device_type=${encodeURIComponent(discoverType)}`, {
headers: getHeaders()
});
const resp = await fetchWithAuth(`/devices/discover?timeout=2&device_type=${encodeURIComponent(discoverType)}`);
if (!resp.ok) return;
const data = await resp.json();
const devices = data.devices || [];

View File

@@ -505,7 +505,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
setTimeout(() => document.getElementById('kc-editor-name').focus(), 100);
} catch (error) {
console.error('Failed to open KC editor:', error);
showToast('Failed to open key colors editor', 'error');
showToast(t('kc_target.error.editor_open_failed'), 'error');
}
}
@@ -588,13 +588,13 @@ export async function saveKCEditor() {
export async function cloneKCTarget(targetId) {
try {
const resp = await fetch(`${API_BASE}/picture-targets/${targetId}`, { headers: getHeaders() });
const resp = await fetchWithAuth(`/picture-targets/${targetId}`);
if (!resp.ok) throw new Error('Failed to load target');
const target = await resp.json();
showKCEditor(null, target);
} catch (error) {
console.error('Failed to clone KC target:', error);
showToast('Failed to clone key colors target', 'error');
if (error.isAuth) return;
showToast(t('kc_target.error.clone_failed'), 'error');
}
}
@@ -613,11 +613,11 @@ export async function deleteKCTarget(targetId) {
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} else {
const error = await response.json();
showToast(`Failed to delete: ${error.detail}`, 'error');
showToast(t('kc_target.error.delete_failed'), 'error');
}
} catch (error) {
if (error.isAuth) return;
showToast('Failed to delete key colors target', 'error');
showToast(t('kc_target.error.delete_failed'), 'error');
}
}

View File

@@ -125,7 +125,7 @@ export async function showPatternTemplateEditor(templateId = null, cloneData = n
setTimeout(() => document.getElementById('pattern-template-name').focus(), 100);
} catch (error) {
console.error('Failed to open pattern template editor:', error);
showToast('Failed to open pattern template editor', 'error');
showToast(t('pattern.error.editor_open_failed'), 'error');
}
}
@@ -189,13 +189,13 @@ export async function savePatternTemplate() {
export async function clonePatternTemplate(templateId) {
try {
const resp = await fetch(`${API_BASE}/pattern-templates/${templateId}`, { headers: getHeaders() });
const resp = await fetchWithAuth(`/pattern-templates/${templateId}`);
if (!resp.ok) throw new Error('Failed to load pattern template');
const tmpl = await resp.json();
showPatternTemplateEditor(null, tmpl);
} catch (error) {
console.error('Failed to clone pattern template:', error);
showToast('Failed to clone pattern template', 'error');
if (error.isAuth) return;
showToast(t('pattern.error.clone_failed'), 'error');
}
}
@@ -216,11 +216,11 @@ export async function deletePatternTemplate(templateId) {
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} else {
const error = await response.json();
showToast(`Failed to delete: ${error.detail}`, 'error');
showToast(t('pattern.error.delete_failed'), 'error');
}
} catch (error) {
if (error.isAuth) return;
showToast('Failed to delete pattern template', 'error');
showToast(t('pattern.error.delete_failed'), 'error');
}
}
@@ -825,6 +825,6 @@ export async function capturePatternBackground() {
}
} catch (error) {
console.error('Failed to capture background:', error);
showToast('Failed to capture background', 'error');
showToast(t('pattern.error.capture_bg_failed'), 'error');
}
}

View File

@@ -144,9 +144,13 @@ function _pushSample(key, value) {
if (_history[key].length > MAX_SAMPLES) _history[key].shift();
const chart = _charts[key];
if (!chart) return;
chart.data.datasets[0].data = [..._history[key]];
chart.data.labels = _history[key].map(() => '');
chart.update();
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() {

View File

@@ -242,19 +242,26 @@ function restoreCaptureDuration() {
}
export async function showTestTemplateModal(templateId) {
const templates = await fetchWithAuth('/capture-templates').then(r => r.json());
const template = templates.templates.find(t => t.id === templateId);
try {
const resp = await fetchWithAuth('/capture-templates');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
const template = (data.templates || []).find(tp => tp.id === templateId);
if (!template) {
if (!template) {
showToast(t('templates.error.load'), 'error');
return;
}
window.currentTestingTemplate = template;
await loadDisplaysForTest();
restoreCaptureDuration();
testTemplateModal.open();
} catch (error) {
if (error.isAuth) return;
showToast(t('templates.error.load'), 'error');
return;
}
window.currentTestingTemplate = template;
await loadDisplaysForTest();
restoreCaptureDuration();
testTemplateModal.open();
}
export function closeTestTemplateModal() {
@@ -871,7 +878,7 @@ export async function cloneAudioTemplate(templateId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone audio template:', error);
showToast('Failed to clone audio template', 'error');
showToast(t('audio_template.error.clone_failed'), 'error');
}
}
@@ -2213,7 +2220,7 @@ export async function cloneStream(streamId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone stream:', error);
showToast('Failed to clone picture source', 'error');
showToast(t('stream.error.clone_picture_failed'), 'error');
}
}
@@ -2226,7 +2233,7 @@ export async function cloneCaptureTemplate(templateId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone capture template:', error);
showToast('Failed to clone capture template', 'error');
showToast(t('stream.error.clone_capture_failed'), 'error');
}
}
@@ -2239,7 +2246,7 @@ export async function clonePPTemplate(templateId) {
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone PP template:', error);
showToast('Failed to clone postprocessing template', 'error');
showToast(t('stream.error.clone_pp_failed'), 'error');
}
}

View File

@@ -113,9 +113,15 @@ function _updateTargetFpsChart(targetId, fpsTarget) {
if (!chart) return;
const actualH = _targetFpsHistory[targetId] || [];
const currentH = _targetFpsCurrentHistory[targetId] || [];
chart.data.labels = actualH.map(() => '');
chart.data.datasets[0].data = [...actualH];
chart.data.datasets[1].data = [...currentH];
// 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.options.scales.y.max = fpsTarget * 1.15;
chart.update('none');
}
@@ -352,7 +358,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
setTimeout(() => document.getElementById('target-editor-name').focus(), 100);
} catch (error) {
console.error('Failed to open target editor:', error);
showToast('Failed to open target editor', 'error');
showToast(t('target.error.editor_open_failed'), 'error');
}
}
@@ -972,7 +978,7 @@ export async function startTargetProcessing(targetId) {
showToast(t('device.started'), 'success');
} else {
const error = await response.json();
showToast(`Failed to start: ${error.detail}`, 'error');
showToast(t('target.error.start_failed'), 'error');
}
});
}
@@ -986,7 +992,7 @@ export async function stopTargetProcessing(targetId) {
showToast(t('device.stopped'), 'success');
} else {
const error = await response.json();
showToast(`Failed to stop: ${error.detail}`, 'error');
showToast(t('target.error.stop_failed'), 'error');
}
});
}
@@ -1027,7 +1033,7 @@ export async function cloneTarget(targetId) {
showTargetEditor(null, target);
} catch (error) {
console.error('Failed to clone target:', error);
showToast('Failed to clone target', 'error');
showToast(t('target.error.clone_failed'), 'error');
}
}
@@ -1042,11 +1048,11 @@ export async function toggleTargetAutoStart(targetId, enable) {
loadTargetsTab();
} else {
const error = await response.json();
showToast(`Failed: ${error.detail}`, 'error');
showToast(t('target.error.autostart_toggle_failed'), 'error');
}
} catch (error) {
console.error('Failed to toggle auto-start:', error);
showToast('Failed to toggle auto-start', 'error');
showToast(t('target.error.autostart_toggle_failed'), 'error');
}
}
@@ -1062,7 +1068,7 @@ export async function deleteTarget(targetId) {
showToast(t('targets.deleted'), 'success');
} else {
const error = await response.json();
showToast(`Failed to delete: ${error.detail}`, 'error');
showToast(t('target.error.delete_failed'), 'error');
}
});
}

View File

@@ -239,10 +239,11 @@ export async function saveValueSource() {
export async function editValueSource(sourceId) {
try {
const resp = await fetchWithAuth(`/value-sources/${sourceId}`);
if (!resp.ok) throw new Error('fetch failed');
if (!resp.ok) throw new Error(t('value_source.error.load'));
const data = await resp.json();
await showValueSourceModal(data);
} catch (e) {
if (e.isAuth) return;
showToast(e.message, 'error');
}
}
@@ -252,7 +253,7 @@ export async function editValueSource(sourceId) {
export async function cloneValueSource(sourceId) {
try {
const resp = await fetchWithAuth(`/value-sources/${sourceId}`);
if (!resp.ok) throw new Error('fetch failed');
if (!resp.ok) throw new Error(t('value_source.error.load'));
const data = await resp.json();
delete data.id;
data.name = data.name + ' (copy)';