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:
2026-02-20 01:18:29 +03:00
parent 2b90fafb9c
commit 3ae20761a1
41 changed files with 355 additions and 248 deletions

View File

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