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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user