Frontend: structured error handling, state fixes, accessibility, i18n
- Enhance fetchWithAuth with auto-401, retry w/ exponential backoff, timeout - Remove ~40 manual 401 checks across 10 feature files - Fix state: brightness cache setter, manual edit flag resets, static import - Add ARIA: role=dialog/tablist, aria-modal, aria-labelledby, aria-selected - Add focus trapping in Modal base class, aria-expanded on hint toggles - Fix WCAG AA color contrast with --primary-text-color variable - Add i18n pluralization (CLDR rules for en/ru), getCurrentLocale export - Replace hardcoded strings in dashboard.js and profiles.js - Add data-i18n-aria-label support, 20 new keys in en.json and ru.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import {
|
||||
calibrationTestState, EDGE_TEST_COLORS,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { closeTutorial, startCalibrationTutorial } from './tutorials.js';
|
||||
@@ -53,11 +53,10 @@ let _previewRaf = null;
|
||||
export async function showCalibration(deviceId) {
|
||||
try {
|
||||
const [response, displaysResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth(`/devices/${deviceId}`),
|
||||
fetchWithAuth('/config/displays'),
|
||||
]);
|
||||
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (!response.ok) { showToast('Failed to load calibration', 'error'); return; }
|
||||
|
||||
const device = await response.json();
|
||||
@@ -131,6 +130,7 @@ export async function showCalibration(deviceId) {
|
||||
window._calibrationResizeObserver.observe(preview);
|
||||
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load calibration:', error);
|
||||
showToast('Failed to load calibration', 'error');
|
||||
}
|
||||
@@ -626,18 +626,17 @@ export async function toggleTestEdge(edge) {
|
||||
updateCalibrationPreview();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/devices/${deviceId}/calibration/test`, {
|
||||
const response = await fetchWithAuth(`/devices/${deviceId}/calibration/test`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ edges })
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
error.textContent = `Test failed: ${errorData.detail}`;
|
||||
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.style.display = 'block';
|
||||
@@ -696,12 +695,10 @@ export async function saveCalibration() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/devices/${deviceId}/calibration`, {
|
||||
const response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(calibration)
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast('Calibration saved', 'success');
|
||||
calibModal.forceClose();
|
||||
@@ -712,6 +709,7 @@ export async function saveCalibration() {
|
||||
error.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.isAuth) return;
|
||||
console.error('Failed to save calibration:', err);
|
||||
error.textContent = 'Failed to save calibration';
|
||||
error.style.display = 'block';
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
*/
|
||||
|
||||
import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } 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';
|
||||
import { startAutoRefresh } from './tabs.js';
|
||||
@@ -284,9 +283,9 @@ function formatUptime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
if (h > 0) return t('time.hours_minutes', { h, m });
|
||||
if (m > 0) return t('time.minutes_seconds', { m, s });
|
||||
return t('time.seconds', { s });
|
||||
}
|
||||
|
||||
export async function loadDashboard(forceFullRender = false) {
|
||||
@@ -297,11 +296,10 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
|
||||
try {
|
||||
const [targetsResp, profilesResp, devicesResp] = await Promise.all([
|
||||
fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/profiles`, { headers: getHeaders() }).catch(() => null),
|
||||
fetch(`${API_BASE}/devices`, { headers: getHeaders() }).catch(() => null),
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/profiles').catch(() => null),
|
||||
fetchWithAuth('/devices').catch(() => null),
|
||||
]);
|
||||
if (targetsResp.status === 401) { handle401Error(); return; }
|
||||
|
||||
const targetsData = await targetsResp.json();
|
||||
const targets = targetsData.targets || [];
|
||||
@@ -415,6 +413,7 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
startPerfPolling();
|
||||
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load dashboard:', error);
|
||||
container.innerHTML = `<div class="dashboard-no-targets">${t('dashboard.failed')}</div>`;
|
||||
} finally {
|
||||
@@ -427,7 +426,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}) {
|
||||
const metrics = target.metrics || {};
|
||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||
const icon = '⚡';
|
||||
const typeLabel = isLed ? 'LED' : 'Key Colors';
|
||||
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
||||
|
||||
let subtitleParts = [typeLabel];
|
||||
if (isLed) {
|
||||
@@ -558,26 +557,23 @@ function renderDashboardProfile(profile) {
|
||||
export async function dashboardToggleProfile(profileId, enable) {
|
||||
try {
|
||||
const endpoint = enable ? 'enable' : 'disable';
|
||||
const response = await fetch(`${API_BASE}/profiles/${profileId}/${endpoint}`, {
|
||||
const response = await fetchWithAuth(`/profiles/${profileId}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
loadDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to toggle profile', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardStartTarget(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/start`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('device.started'), 'success');
|
||||
loadDashboard();
|
||||
@@ -586,17 +582,16 @@ export async function dashboardStartTarget(targetId) {
|
||||
showToast(`Failed to start: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to start processing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardStopTarget(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/stop`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('device.stopped'), 'success');
|
||||
loadDashboard();
|
||||
@@ -605,21 +600,22 @@ export async function dashboardStopTarget(targetId) {
|
||||
showToast(`Failed to stop: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to stop processing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardStopAll() {
|
||||
try {
|
||||
const targetsResp = await fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() });
|
||||
if (targetsResp.status === 401) { handle401Error(); return; }
|
||||
const targetsResp = await fetchWithAuth('/picture-targets');
|
||||
const data = await targetsResp.json();
|
||||
const running = (data.targets || []).filter(t => t.id);
|
||||
await Promise.all(running.map(t =>
|
||||
fetch(`${API_BASE}/picture-targets/${t.id}/stop`, { method: 'POST', headers: getHeaders() }).catch(() => {})
|
||||
fetchWithAuth(`/picture-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {})
|
||||
));
|
||||
loadDashboard();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to stop all targets', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
_discoveryScanRunning, set_discoveryScanRunning,
|
||||
_discoveryCache, set_discoveryCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, isSerialDevice, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -214,11 +214,7 @@ export async function scanForDevices(forceType) {
|
||||
if (scanBtn) scanBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/devices/discover?timeout=3&device_type=${encodeURIComponent(scanType)}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
const response = await fetchWithAuth(`/devices/discover?timeout=3&device_type=${encodeURIComponent(scanType)}`);
|
||||
|
||||
loading.style.display = 'none';
|
||||
if (scanBtn) scanBtn.disabled = false;
|
||||
@@ -240,6 +236,7 @@ export async function scanForDevices(forceType) {
|
||||
_renderDiscoveryList();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.isAuth) return;
|
||||
loading.style.display = 'none';
|
||||
if (scanBtn) scanBtn.disabled = false;
|
||||
if (!isSerialDevice(scanType)) {
|
||||
@@ -298,17 +295,11 @@ export async function handleAddDevice(event) {
|
||||
body.capture_template_id = lastTemplateId;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/devices`, {
|
||||
const response = await fetchWithAuth('/devices', {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
handle401Error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('Device added successfully:', result);
|
||||
@@ -330,6 +321,7 @@ export async function handleAddDevice(event) {
|
||||
error.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.isAuth) return;
|
||||
console.error('Failed to add device:', err);
|
||||
showToast('Failed to add device', 'error');
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
_deviceBrightnessCache,
|
||||
_deviceBrightnessCache, updateDeviceBrightness,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, escapeHtml, isSerialDevice, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
|
||||
class DeviceSettingsModal extends Modal {
|
||||
@@ -110,18 +110,15 @@ export function createDeviceCard(device) {
|
||||
|
||||
export async function toggleDevicePower(deviceId) {
|
||||
try {
|
||||
const getResp = await fetch(`${API_BASE}/devices/${deviceId}/power`, { headers: getHeaders() });
|
||||
if (getResp.status === 401) { handle401Error(); return; }
|
||||
const getResp = await fetchWithAuth(`/devices/${deviceId}/power`);
|
||||
if (!getResp.ok) { showToast('Failed to get power state', 'error'); return; }
|
||||
const current = await getResp.json();
|
||||
const newState = !current.on;
|
||||
|
||||
const setResp = await fetch(`${API_BASE}/devices/${deviceId}/power`, {
|
||||
const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, {
|
||||
method: 'PUT',
|
||||
headers: { ...getHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ on: newState })
|
||||
});
|
||||
if (setResp.status === 401) { handle401Error(); return; }
|
||||
if (setResp.ok) {
|
||||
showToast(t(newState ? 'device.power.on_success' : 'device.power.off_success'), 'success');
|
||||
} else {
|
||||
@@ -129,6 +126,7 @@ export async function toggleDevicePower(deviceId) {
|
||||
showToast(error.detail || 'Failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to toggle power', 'error');
|
||||
}
|
||||
}
|
||||
@@ -142,11 +140,9 @@ export async function removeDevice(deviceId) {
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
const response = await fetchWithAuth(`/devices/${deviceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast('Device removed', 'success');
|
||||
window.loadDevices();
|
||||
@@ -155,6 +151,7 @@ export async function removeDevice(deviceId) {
|
||||
showToast(`Failed to remove: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to remove device:', error);
|
||||
showToast('Failed to remove device', 'error');
|
||||
}
|
||||
@@ -162,8 +159,7 @@ export async function removeDevice(deviceId) {
|
||||
|
||||
export async function showSettings(deviceId) {
|
||||
try {
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() });
|
||||
if (deviceResponse.status === 401) { handle401Error(); return; }
|
||||
const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`);
|
||||
if (!deviceResponse.ok) { showToast('Failed to load device settings', 'error'); return; }
|
||||
|
||||
const device = await deviceResponse.json();
|
||||
@@ -222,6 +218,7 @@ export async function showSettings(deviceId) {
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load device settings:', error);
|
||||
showToast('Failed to load device settings', 'error');
|
||||
}
|
||||
@@ -251,14 +248,11 @@ export async function saveDeviceSettings() {
|
||||
const baudVal = document.getElementById('settings-baud-rate').value;
|
||||
if (baudVal) body.baud_rate = parseInt(baudVal, 10);
|
||||
}
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (deviceResponse.status === 401) { handle401Error(); return; }
|
||||
|
||||
if (!deviceResponse.ok) {
|
||||
const errorData = await deviceResponse.json();
|
||||
settingsModal.showError(`Failed to update device: ${errorData.detail}`);
|
||||
@@ -269,6 +263,7 @@ export async function saveDeviceSettings() {
|
||||
settingsModal.forceClose();
|
||||
window.loadDevices();
|
||||
} catch (err) {
|
||||
if (err.isAuth) return;
|
||||
console.error('Failed to save device settings:', err);
|
||||
settingsModal.showError('Failed to save settings');
|
||||
}
|
||||
@@ -282,7 +277,7 @@ export function updateBrightnessLabel(deviceId, value) {
|
||||
|
||||
export async function saveCardBrightness(deviceId, value) {
|
||||
const bri = parseInt(value);
|
||||
_deviceBrightnessCache[deviceId] = bri;
|
||||
updateDeviceBrightness(deviceId, bri);
|
||||
try {
|
||||
await fetch(`${API_BASE}/devices/${deviceId}/brightness`, {
|
||||
method: 'PUT',
|
||||
@@ -302,7 +297,7 @@ export async function fetchDeviceBrightness(deviceId) {
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
_deviceBrightnessCache[deviceId] = data.brightness;
|
||||
updateDeviceBrightness(deviceId, data.brightness);
|
||||
const slider = document.querySelector(`[data-device-brightness="${deviceId}"]`);
|
||||
if (slider) {
|
||||
slider.value = data.brightness;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
kcWebSockets,
|
||||
PATTERN_RECT_BORDERS,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { lockBody, showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -436,10 +436,12 @@ export function isKCEditorDirty() {
|
||||
|
||||
export async function closeKCEditorModal() {
|
||||
await kcEditorModal.close();
|
||||
set_kcNameManuallyEdited(false);
|
||||
}
|
||||
|
||||
export function forceCloseKCEditorModal() {
|
||||
kcEditorModal.forceClose();
|
||||
set_kcNameManuallyEdited(false);
|
||||
}
|
||||
|
||||
export async function saveKCEditor() {
|
||||
@@ -475,22 +477,18 @@ export async function saveKCEditor() {
|
||||
try {
|
||||
let response;
|
||||
if (targetId) {
|
||||
response = await fetch(`${API_BASE}/picture-targets/${targetId}`, {
|
||||
response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
payload.target_type = 'key_colors';
|
||||
response = await fetch(`${API_BASE}/picture-targets`, {
|
||||
response = await fetchWithAuth('/picture-targets', {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Failed to save');
|
||||
@@ -501,6 +499,7 @@ export async function saveKCEditor() {
|
||||
// Use window.* to avoid circular import with targets.js
|
||||
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Error saving KC target:', error);
|
||||
kcEditorModal.showError(error.message);
|
||||
}
|
||||
@@ -512,11 +511,9 @@ export async function deleteKCTarget(targetId) {
|
||||
|
||||
try {
|
||||
disconnectKCWebSocket(targetId);
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('kc.deleted'), 'success');
|
||||
// Use window.* to avoid circular import with targets.js
|
||||
@@ -526,6 +523,7 @@ export async function deleteKCTarget(targetId) {
|
||||
showToast(`Failed to delete: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to delete key colors target', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
PATTERN_RECT_COLORS,
|
||||
PATTERN_RECT_BORDERS,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -89,13 +89,13 @@ export async function showPatternTemplateEditor(templateId = null) {
|
||||
document.getElementById('pattern-template-id').value = tmpl.id;
|
||||
document.getElementById('pattern-template-name').value = tmpl.name;
|
||||
document.getElementById('pattern-template-description').value = tmpl.description || '';
|
||||
document.getElementById('pattern-template-title').textContent = t('pattern.edit');
|
||||
document.getElementById('pattern-template-modal-title').textContent = t('pattern.edit');
|
||||
setPatternEditorRects((tmpl.rectangles || []).map(r => ({ ...r })));
|
||||
} else {
|
||||
document.getElementById('pattern-template-id').value = '';
|
||||
document.getElementById('pattern-template-name').value = '';
|
||||
document.getElementById('pattern-template-description').value = '';
|
||||
document.getElementById('pattern-template-title').textContent = t('pattern.add');
|
||||
document.getElementById('pattern-template-modal-title').textContent = t('pattern.add');
|
||||
setPatternEditorRects([]);
|
||||
}
|
||||
|
||||
@@ -148,16 +148,15 @@ export async function savePatternTemplate() {
|
||||
try {
|
||||
let response;
|
||||
if (templateId) {
|
||||
response = await fetch(`${API_BASE}/pattern-templates/${templateId}`, {
|
||||
method: 'PUT', headers: getHeaders(), body: JSON.stringify(payload),
|
||||
response = await fetchWithAuth(`/pattern-templates/${templateId}`, {
|
||||
method: 'PUT', body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${API_BASE}/pattern-templates`, {
|
||||
method: 'POST', headers: getHeaders(), body: JSON.stringify(payload),
|
||||
response = await fetchWithAuth('/pattern-templates', {
|
||||
method: 'POST', body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Failed to save');
|
||||
@@ -168,6 +167,7 @@ export async function savePatternTemplate() {
|
||||
// Use window.* to avoid circular import with targets.js
|
||||
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Error saving pattern template:', error);
|
||||
patternModal.showError(error.message);
|
||||
}
|
||||
@@ -178,10 +178,9 @@ export async function deletePatternTemplate(templateId) {
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/pattern-templates/${templateId}`, {
|
||||
method: 'DELETE', headers: getHeaders(),
|
||||
const response = await fetchWithAuth(`/pattern-templates/${templateId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.status === 409) {
|
||||
showToast(t('pattern.delete.referenced'), 'error');
|
||||
return;
|
||||
@@ -194,6 +193,7 @@ export async function deletePatternTemplate(templateId) {
|
||||
showToast(`Failed to delete: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to delete pattern template', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { apiKey, _profilesCache, set_profilesCache } from '../core/state.js';
|
||||
import { API_BASE, getHeaders, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -25,12 +25,13 @@ export async function loadProfiles() {
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/profiles`, { headers: getHeaders() });
|
||||
const resp = await fetchWithAuth('/profiles');
|
||||
if (!resp.ok) throw new Error('Failed to load profiles');
|
||||
const data = await resp.json();
|
||||
set_profilesCache(data.profiles);
|
||||
renderProfiles(data.profiles);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load profiles:', error);
|
||||
container.innerHTML = `<p class="error-message">${error.message}</p>`;
|
||||
}
|
||||
@@ -72,7 +73,7 @@ function createProfileCard(profile) {
|
||||
}
|
||||
return `<span class="stream-card-prop">${c.condition_type}</span>`;
|
||||
});
|
||||
const logicLabel = profile.condition_logic === 'and' ? ' AND ' : ' OR ';
|
||||
const logicLabel = profile.condition_logic === 'and' ? t('profiles.logic.and') : t('profiles.logic.or');
|
||||
condPills = parts.join(`<span class="profile-logic-label">${logicLabel}</span>`);
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ function createProfileCard(profile) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-subtitle">
|
||||
<span class="card-meta">${profile.condition_logic === 'and' ? 'ALL' : 'ANY'}</span>
|
||||
<span class="card-meta">${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')}</span>
|
||||
<span class="card-meta">⚡ ${targetCountText}</span>
|
||||
${lastActivityMeta}
|
||||
</div>
|
||||
@@ -128,7 +129,7 @@ export async function openProfileEditor(profileId) {
|
||||
if (profileId) {
|
||||
titleEl.textContent = t('profiles.edit');
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/profiles/${profileId}`, { headers: getHeaders() });
|
||||
const resp = await fetchWithAuth(`/profiles/${profileId}`);
|
||||
if (!resp.ok) throw new Error('Failed to load profile');
|
||||
const profile = await resp.json();
|
||||
|
||||
@@ -167,7 +168,7 @@ export function closeProfileEditorModal() {
|
||||
async function loadProfileTargetChecklist(selectedIds) {
|
||||
const container = document.getElementById('profile-targets-list');
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() });
|
||||
const resp = await fetchWithAuth('/picture-targets');
|
||||
if (!resp.ok) throw new Error('Failed to load targets');
|
||||
const data = await resp.json();
|
||||
const targets = data.targets || [];
|
||||
@@ -253,8 +254,7 @@ async function toggleProcessPicker(picker, row) {
|
||||
picker.style.display = '';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/system/processes`, { headers: getHeaders() });
|
||||
if (resp.status === 401) { handle401Error(); return; }
|
||||
const resp = await fetchWithAuth('/system/processes');
|
||||
if (!resp.ok) throw new Error('Failed to fetch processes');
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -326,7 +326,7 @@ export async function saveProfileEditor() {
|
||||
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) {
|
||||
profileModal.showError('Name is required');
|
||||
profileModal.showError(t('profiles.error.name_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,10 +342,9 @@ export async function saveProfileEditor() {
|
||||
const isEdit = !!profileId;
|
||||
|
||||
try {
|
||||
const url = isEdit ? `${API_BASE}/profiles/${profileId}` : `${API_BASE}/profiles`;
|
||||
const resp = await fetch(url, {
|
||||
const url = isEdit ? `/profiles/${profileId}` : '/profiles';
|
||||
const resp = await fetchWithAuth(url, {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
@@ -354,9 +353,10 @@ export async function saveProfileEditor() {
|
||||
}
|
||||
|
||||
profileModal.forceClose();
|
||||
showToast(isEdit ? 'Profile updated' : 'Profile created', 'success');
|
||||
showToast(isEdit ? t('profiles.updated') : t('profiles.created'), 'success');
|
||||
loadProfiles();
|
||||
} catch (e) {
|
||||
if (e.isAuth) return;
|
||||
profileModal.showError(e.message);
|
||||
}
|
||||
}
|
||||
@@ -364,13 +364,13 @@ export async function saveProfileEditor() {
|
||||
export async function toggleProfileEnabled(profileId, enable) {
|
||||
try {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
const resp = await fetch(`${API_BASE}/profiles/${profileId}/${action}`, {
|
||||
const resp = await fetchWithAuth(`/profiles/${profileId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Failed to ${action} profile`);
|
||||
loadProfiles();
|
||||
} catch (e) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
@@ -381,14 +381,14 @@ export async function deleteProfile(profileId, profileName) {
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/profiles/${profileId}`, {
|
||||
const resp = await fetchWithAuth(`/profiles/${profileId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders(),
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to delete profile');
|
||||
showToast('Profile deleted', 'success');
|
||||
showToast(t('profiles.deleted'), 'success');
|
||||
loadProfiles();
|
||||
} catch (e) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
_lastValidatedImageSource, set_lastValidatedImageSource,
|
||||
apiKey,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js';
|
||||
@@ -105,6 +105,7 @@ export async function editTemplate(templateId) {
|
||||
export function closeTemplateModal() {
|
||||
templateModal.forceClose();
|
||||
setCurrentEditingTemplateId(null);
|
||||
set_templateNameManuallyEdited(false);
|
||||
}
|
||||
|
||||
function updateCaptureDuration(value) {
|
||||
@@ -900,6 +901,7 @@ export async function deleteStream(streamId) {
|
||||
export function closeStreamModal() {
|
||||
streamModal.forceClose();
|
||||
document.getElementById('stream-type').disabled = false;
|
||||
set_streamNameManuallyEdited(false);
|
||||
}
|
||||
|
||||
async function validateStaticImage() {
|
||||
@@ -1375,6 +1377,7 @@ export async function deletePPTemplate(templateId) {
|
||||
export function closePPTemplateModal() {
|
||||
ppTemplateModal.forceClose();
|
||||
set_modalFilters([]);
|
||||
set_ppTemplateNameManuallyEdited(false);
|
||||
}
|
||||
|
||||
// Exported helpers used by other modules
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import { apiKey, refreshInterval, setRefreshInterval, dashboardPollInterval } from '../core/state.js';
|
||||
|
||||
export function switchTab(name) {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
const isActive = btn.dataset.tab === name;
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-selected', String(isActive));
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||
localStorage.setItem('activeTab', name);
|
||||
if (name === 'dashboard') {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
_deviceBrightnessCache,
|
||||
kcWebSockets,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -194,22 +194,18 @@ export async function saveTargetEditor() {
|
||||
try {
|
||||
let response;
|
||||
if (targetId) {
|
||||
response = await fetch(`${API_BASE}/picture-targets/${targetId}`, {
|
||||
response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
payload.target_type = 'led';
|
||||
response = await fetch(`${API_BASE}/picture-targets`, {
|
||||
response = await fetchWithAuth('/picture-targets', {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Failed to save');
|
||||
@@ -219,6 +215,7 @@ export async function saveTargetEditor() {
|
||||
targetEditorModal.forceClose();
|
||||
await loadTargetsTab();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Error saving target:', error);
|
||||
targetEditorModal.showError(error.message);
|
||||
}
|
||||
@@ -248,17 +245,12 @@ export async function loadTargetsTab() {
|
||||
try {
|
||||
// Fetch devices, targets, sources, and pattern templates in parallel
|
||||
const [devicesResp, targetsResp, sourcesResp, patResp] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/devices'),
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/picture-sources').catch(() => null),
|
||||
fetchWithAuth('/pattern-templates').catch(() => null),
|
||||
]);
|
||||
|
||||
if (devicesResp.status === 401 || targetsResp.status === 401) {
|
||||
handle401Error();
|
||||
return;
|
||||
}
|
||||
|
||||
const devicesData = await devicesResp.json();
|
||||
const devices = devicesData.devices || [];
|
||||
|
||||
@@ -424,6 +416,7 @@ export async function loadTargetsTab() {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load targets tab:', error);
|
||||
container.innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
||||
}
|
||||
@@ -548,11 +541,9 @@ export function createTargetCard(target, deviceMap, sourceMap) {
|
||||
|
||||
export async function startTargetProcessing(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/start`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('device.started'), 'success');
|
||||
loadTargetsTab();
|
||||
@@ -561,17 +552,16 @@ export async function startTargetProcessing(targetId) {
|
||||
showToast(`Failed to start: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to start processing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopTargetProcessing(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/stop`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('device.stopped'), 'success');
|
||||
loadTargetsTab();
|
||||
@@ -580,17 +570,16 @@ export async function stopTargetProcessing(targetId) {
|
||||
showToast(`Failed to stop: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to stop processing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function startTargetOverlay(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/overlay/start`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('overlay.started'), 'success');
|
||||
loadTargetsTab();
|
||||
@@ -599,17 +588,16 @@ export async function startTargetOverlay(targetId) {
|
||||
showToast(t('overlay.error.start') + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(t('overlay.error.start'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopTargetOverlay(targetId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/overlay/stop`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/stop`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('overlay.stopped'), 'success');
|
||||
loadTargetsTab();
|
||||
@@ -618,6 +606,7 @@ export async function stopTargetOverlay(targetId) {
|
||||
showToast(t('overlay.error.stop') + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(t('overlay.error.stop'), 'error');
|
||||
}
|
||||
}
|
||||
@@ -627,11 +616,9 @@ export async function deleteTarget(targetId) {
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/picture-targets/${targetId}`, {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (response.status === 401) { handle401Error(); return; }
|
||||
if (response.ok) {
|
||||
showToast(t('targets.deleted'), 'success');
|
||||
loadTargetsTab();
|
||||
@@ -640,6 +627,7 @@ export async function deleteTarget(targetId) {
|
||||
showToast(`Failed to delete: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast('Failed to delete target', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user