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:
@@ -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