diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css index 8070b3a..4da43e4 100644 --- a/server/src/wled_controller/static/css/base.css +++ b/server/src/wled_controller/static/css/base.css @@ -18,6 +18,7 @@ --text-color: #e0e0e0; --border-color: #404040; --display-badge-bg: rgba(0, 0, 0, 0.4); + --primary-text-color: #66bb6a; color-scheme: dark; } @@ -28,6 +29,7 @@ --text-color: #333333; --border-color: #e0e0e0; --display-badge-bg: rgba(255, 255, 255, 0.85); + --primary-text-color: #3d8b40; color-scheme: light; } diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index b810201..afbf1c3 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -40,7 +40,7 @@ section { } .add-device-card:hover .add-device-icon { - color: var(--primary-color); + color: var(--primary-text-color); } .add-device-label { @@ -86,7 +86,7 @@ section { .card-tutorial-btn:hover { border-color: var(--primary-color); - color: var(--primary-color); + color: var(--primary-text-color); } @@ -119,7 +119,7 @@ section { } .card-power-btn:hover { - color: var(--primary-color); + color: var(--primary-text-color); background: rgba(76, 175, 80, 0.1); } @@ -319,7 +319,7 @@ section { } .primary-star { - color: var(--primary-color); + color: var(--primary-text-color); font-size: 1.2rem; } @@ -400,7 +400,7 @@ section { position: absolute; top: 2px; right: 4px; - color: var(--primary-color); + color: var(--primary-text-color); font-size: 1.5rem; line-height: 1; text-shadow: 0 0 4px rgba(0, 0, 0, 0.4); @@ -511,7 +511,7 @@ ul.section-tip li { .metric-value { font-size: 0.9rem; font-weight: 700; - color: var(--primary-color); + color: var(--primary-text-color); } .metric-label { @@ -535,7 +535,7 @@ ul.section-tip li { .timing-total { font-size: 0.8rem; - color: var(--primary-color); + color: var(--primary-text-color); } .timing-bar { diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css index e1b9e32..8cddbca 100644 --- a/server/src/wled_controller/static/css/dashboard.css +++ b/server/src/wled_controller/static/css/dashboard.css @@ -133,7 +133,7 @@ .dashboard-metric-value { font-size: 0.85rem; font-weight: 700; - color: var(--primary-color); + color: var(--primary-text-color); line-height: 1.2; } diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css index bbfcbfb..e37b1ce 100644 --- a/server/src/wled_controller/static/css/layout.css +++ b/server/src/wled_controller/static/css/layout.css @@ -16,7 +16,7 @@ header { h1 { font-size: 2rem; - color: var(--primary-color); + color: var(--primary-text-color); } h2 { @@ -103,7 +103,7 @@ h2 { .health-latency { font-size: 0.7rem; font-weight: 400; - color: #4CAF50; + color: var(--primary-text-color); margin-left: auto; padding-left: 8px; opacity: 0.85; @@ -140,7 +140,7 @@ h2 { } .tab-btn.active { - color: var(--primary-color); + color: var(--primary-text-color); border-bottom-color: var(--primary-color); } @@ -190,7 +190,7 @@ h2 { } .footer-content a { - color: var(--primary-color); + color: var(--primary-text-color); text-decoration: none; transition: opacity 0.2s; } diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index 5156752..85a06b4 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -185,7 +185,7 @@ .hint-toggle.active { opacity: 1; - color: var(--primary-color, #4CAF50); + color: var(--primary-text-color, #4CAF50); border-color: var(--primary-color, #4CAF50); } diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 7b90a32..4c39916 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -50,7 +50,7 @@ } .add-template-card:hover .add-template-icon { - color: var(--primary-color); + color: var(--primary-text-color); } .add-template-label { @@ -126,7 +126,7 @@ .template-config-details summary { cursor: pointer; - color: var(--primary-color); + color: var(--primary-text-color); font-weight: 500; padding: 4px 0; } @@ -138,7 +138,7 @@ .template-no-config { margin: 12px 0; font-size: 13px; - color: var(--primary-color); + color: var(--primary-text-color); font-weight: 500; padding: 4px 0; } @@ -558,7 +558,7 @@ } .stream-tab-btn.active { - color: var(--primary-color); + color: var(--primary-text-color); border-bottom-color: var(--primary-color); } diff --git a/server/src/wled_controller/static/css/tutorials.css b/server/src/wled_controller/static/css/tutorials.css index a22db99..c569daf 100644 --- a/server/src/wled_controller/static/css/tutorials.css +++ b/server/src/wled_controller/static/css/tutorials.css @@ -5,7 +5,7 @@ border-radius: 50%; border: 2px solid var(--primary-color); background: transparent; - color: var(--primary-color); + color: var(--primary-text-color); font-size: 1rem; font-weight: bold; cursor: pointer; @@ -95,7 +95,7 @@ .tutorial-step-counter { font-size: 0.8rem; font-weight: 600; - color: var(--primary-color); + color: var(--primary-text-color); } .tutorial-close-btn { diff --git a/server/src/wled_controller/static/js/core/api.js b/server/src/wled_controller/static/js/core/api.js index 98b8467..4a6bd02 100644 --- a/server/src/wled_controller/static/js/core/api.js +++ b/server/src/wled_controller/static/js/core/api.js @@ -2,7 +2,7 @@ * API utilities — base URL, auth headers, fetch wrapper, helpers. */ -import { apiKey, setApiKey, refreshInterval, setRefreshInterval } from './state.js'; +import { apiKey, setApiKey, refreshInterval, setRefreshInterval, set_cachedDisplays } from './state.js'; export const API_BASE = '/api/v1'; @@ -16,12 +16,51 @@ export function getHeaders() { return headers; } +export class ApiError extends Error { + constructor(status, message) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.isAuth = status === 401; + } +} + export async function fetchWithAuth(url, options = {}) { + const { retry = true, timeout = 10000, handle401: auto401 = true, ...fetchOpts } = options; const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; - const headers = options.headers - ? { ...getHeaders(), ...options.headers } + const headers = fetchOpts.headers + ? { ...getHeaders(), ...fetchOpts.headers } : getHeaders(); - return fetch(fullUrl, { ...options, headers }); + + const maxAttempts = retry ? 3 : 1; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const controller = new AbortController(); + if (fetchOpts.signal) { + fetchOpts.signal.addEventListener('abort', () => controller.abort()); + } + const timer = setTimeout(() => controller.abort(), timeout); + try { + const resp = await fetch(fullUrl, { ...fetchOpts, headers, signal: controller.signal }); + clearTimeout(timer); + if (resp.status === 401 && auto401) { + handle401Error(); + throw new ApiError(401, 'Session expired'); + } + if (resp.status >= 500 && attempt < maxAttempts - 1) { + await new Promise(r => setTimeout(r, 500 * 2 ** attempt)); + continue; + } + return resp; + } catch (err) { + clearTimeout(timer); + if (err instanceof ApiError) throw err; + if (attempt < maxAttempts - 1) { + await new Promise(r => setTimeout(r, 500 * 2 ** attempt)); + continue; + } + throw err; + } + } } export function escapeHtml(text) { @@ -36,6 +75,7 @@ export function isSerialDevice(type) { } export function handle401Error() { + if (!apiKey) return; // Already handled or no session localStorage.removeItem('wled_api_key'); setApiKey(null); @@ -80,23 +120,14 @@ export async function loadServerInfo() { export async function loadDisplays() { try { - const response = await fetch(`${API_BASE}/config/displays`, { - headers: getHeaders() - }); - - if (response.status === 401) { - handle401Error(); - return; - } - + const response = await fetchWithAuth('/config/displays'); const data = await response.json(); if (data.displays && data.displays.length > 0) { - // Import setter to update shared state - const { set_cachedDisplays } = await import('./state.js'); set_cachedDisplays(data.displays); } } catch (error) { + if (error instanceof ApiError && error.isAuth) return; console.error('Failed to load displays:', error); } } diff --git a/server/src/wled_controller/static/js/core/i18n.js b/server/src/wled_controller/static/js/core/i18n.js index f7e1d4d..88454c9 100644 --- a/server/src/wled_controller/static/js/core/i18n.js +++ b/server/src/wled_controller/static/js/core/i18n.js @@ -17,8 +17,26 @@ const fallbackTranslations = { 'auth.button.login': 'Login' }; +export function getCurrentLocale() { return currentLocale; } + +function getPluralForm(locale, count) { + if (locale === 'ru') { + const mod10 = count % 10, mod100 = count % 100; + if (mod10 === 1 && mod100 !== 11) return 'one'; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'few'; + return 'many'; + } + return count === 1 ? 'one' : 'other'; +} + export function t(key, params = {}) { - let text = translations[key] || fallbackTranslations[key] || key; + let text; + if ('count' in params) { + const form = getPluralForm(currentLocale, params.count); + text = translations[`${key}.${form}`] || translations[key] || fallbackTranslations[key] || key; + } else { + text = translations[key] || fallbackTranslations[key] || key; + } Object.keys(params).forEach(param => { text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); }); @@ -99,6 +117,11 @@ export function updateAllText() { el.title = t(key); }); + document.querySelectorAll('[data-i18n-aria-label]').forEach(el => { + const key = el.getAttribute('data-i18n-aria-label'); + el.setAttribute('aria-label', t(key)); + }); + // Notify subscribers that the language changed (skip initial load) if (_initialized) { document.dispatchEvent(new CustomEvent('languageChanged')); diff --git a/server/src/wled_controller/static/js/core/modal.js b/server/src/wled_controller/static/js/core/modal.js index 0fe0179..47bd1eb 100644 --- a/server/src/wled_controller/static/js/core/modal.js +++ b/server/src/wled_controller/static/js/core/modal.js @@ -6,7 +6,7 @@ */ import { t } from './i18n.js'; -import { lockBody, unlockBody, setupBackdropClose, showConfirm } from './ui.js'; +import { lockBody, unlockBody, setupBackdropClose, showConfirm, trapFocus, releaseFocus } from './ui.js'; export class Modal { static _stack = []; @@ -25,20 +25,27 @@ export class Modal { } open() { + this._previousFocus = document.activeElement; this.el.style.display = 'flex'; if (this._lock) lockBody(); if (this._backdrop) setupBackdropClose(this.el, () => this.close()); + trapFocus(this.el); Modal._stack = Modal._stack.filter(m => m !== this); Modal._stack.push(this); } forceClose() { + releaseFocus(this.el); this.el.style.display = 'none'; if (this._lock) unlockBody(); this._initialValues = {}; this.hideError(); this.onForceClose(); Modal._stack = Modal._stack.filter(m => m !== this); + if (this._previousFocus && typeof this._previousFocus.focus === 'function') { + this._previousFocus.focus(); + this._previousFocus = null; + } } async close() { diff --git a/server/src/wled_controller/static/js/core/state.js b/server/src/wled_controller/static/js/core/state.js index 37fc042..96b11ce 100644 --- a/server/src/wled_controller/static/js/core/state.js +++ b/server/src/wled_controller/static/js/core/state.js @@ -41,7 +41,11 @@ export const EDGE_TEST_COLORS = { export const loggedErrors = new Map(); // Device brightness cache -export const _deviceBrightnessCache = {}; +export let _deviceBrightnessCache = {}; +export function set_deviceBrightnessCache(v) { _deviceBrightnessCache = v; } +export function updateDeviceBrightness(deviceId, value) { + _deviceBrightnessCache = { ..._deviceBrightnessCache, [deviceId]: value }; +} // Discovery state export let _discoveryScanRunning = false; diff --git a/server/src/wled_controller/static/js/core/ui.js b/server/src/wled_controller/static/js/core/ui.js index 88b6222..4b8b58e 100644 --- a/server/src/wled_controller/static/js/core/ui.js +++ b/server/src/wled_controller/static/js/core/ui.js @@ -11,6 +11,32 @@ export function toggleHint(btn) { const visible = hint.style.display !== 'none'; hint.style.display = visible ? 'none' : 'block'; btn.classList.toggle('active', !visible); + btn.setAttribute('aria-expanded', String(!visible)); + } +} + +const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; + +export function trapFocus(modal) { + modal._trapHandler = (e) => { + if (e.key !== 'Tab') return; + const focusable = [...modal.querySelectorAll(FOCUSABLE)].filter(el => el.offsetParent !== null); + if (!focusable.length) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first.focus(); } + } + }; + modal.addEventListener('keydown', modal._trapHandler); +} + +export function releaseFocus(modal) { + if (modal._trapHandler) { + modal.removeEventListener('keydown', modal._trapHandler); + modal._trapHandler = null; } } @@ -111,7 +137,7 @@ export function showConfirm(message, title = null) { setConfirmResolve(resolve); const modal = document.getElementById('confirm-modal'); - const titleEl = document.getElementById('confirm-title'); + const titleEl = document.getElementById('confirm-modal-title'); const messageEl = document.getElementById('confirm-message'); const yesBtn = document.getElementById('confirm-yes-btn'); const noBtn = document.getElementById('confirm-no-btn'); @@ -123,11 +149,13 @@ export function showConfirm(message, title = null) { modal.style.display = 'flex'; lockBody(); + trapFocus(modal); }); } export function closeConfirmModal(result) { const modal = document.getElementById('confirm-modal'); + releaseFocus(modal); modal.style.display = 'none'; unlockBody(); diff --git a/server/src/wled_controller/static/js/features/calibration.js b/server/src/wled_controller/static/js/features/calibration.js index ab3f961..b104d49 100644 --- a/server/src/wled_controller/static/js/features/calibration.js +++ b/server/src/wled_controller/static/js/features/calibration.js @@ -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'; diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index 507d95d..29a212b 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -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 = `
${t('dashboard.failed')}
`; } 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'); } } diff --git a/server/src/wled_controller/static/js/features/device-discovery.js b/server/src/wled_controller/static/js/features/device-discovery.js index a3db514..564957b 100644 --- a/server/src/wled_controller/static/js/features/device-discovery.js +++ b/server/src/wled_controller/static/js/features/device-discovery.js @@ -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'); } diff --git a/server/src/wled_controller/static/js/features/devices.js b/server/src/wled_controller/static/js/features/devices.js index 18949bb..e906620 100644 --- a/server/src/wled_controller/static/js/features/devices.js +++ b/server/src/wled_controller/static/js/features/devices.js @@ -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; diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index 901d4b4..14f15e7 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -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'); } } diff --git a/server/src/wled_controller/static/js/features/pattern-templates.js b/server/src/wled_controller/static/js/features/pattern-templates.js index c74d417..063a636 100644 --- a/server/src/wled_controller/static/js/features/pattern-templates.js +++ b/server/src/wled_controller/static/js/features/pattern-templates.js @@ -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'); } } diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/profiles.js index 123f232..6dd2fc0 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/profiles.js @@ -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 = `

${error.message}

`; } @@ -72,7 +73,7 @@ function createProfileCard(profile) { } return `${c.condition_type}`; }); - 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(`${logicLabel}`); } @@ -96,7 +97,7 @@ function createProfileCard(profile) {
- ${profile.condition_logic === 'and' ? 'ALL' : 'ANY'} + ${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')} ⚡ ${targetCountText} ${lastActivityMeta}
@@ -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'); } } diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 09b3308..c2d1022 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -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 diff --git a/server/src/wled_controller/static/js/features/tabs.js b/server/src/wled_controller/static/js/features/tabs.js index f34af9a..5bd9ba4 100644 --- a/server/src/wled_controller/static/js/features/tabs.js +++ b/server/src/wled_controller/static/js/features/tabs.js @@ -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') { diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index bc80b88..86e07f9 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -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 = `
${t('targets.failed')}
`; } @@ -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'); } } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 592adfb..3115e44 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -512,5 +512,24 @@ "profiles.status.active": "Active", "profiles.status.inactive": "Inactive", "profiles.status.disabled": "Disabled", - "profiles.last_activated": "Last activated" + "profiles.last_activated": "Last activated", + "profiles.logic.and": " AND ", + "profiles.logic.or": " OR ", + "profiles.logic.all": "ALL", + "profiles.logic.any": "ANY", + "profiles.updated": "Profile updated", + "profiles.created": "Profile created", + "profiles.deleted": "Profile deleted", + "profiles.error.name_required": "Name is required", + "time.hours_minutes": "{h}h {m}m", + "time.minutes_seconds": "{m}m {s}s", + "time.seconds": "{s}s", + "dashboard.type.led": "LED", + "dashboard.type.kc": "Key Colors", + "aria.close": "Close", + "aria.save": "Save", + "aria.cancel": "Cancel", + "aria.previous": "Previous", + "aria.next": "Next", + "aria.hint": "Show hint" } diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 1b165e0..dd034ed 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -512,5 +512,24 @@ "profiles.status.active": "Активен", "profiles.status.inactive": "Неактивен", "profiles.status.disabled": "Отключён", - "profiles.last_activated": "Последняя активация" + "profiles.last_activated": "Последняя активация", + "profiles.logic.and": " И ", + "profiles.logic.or": " ИЛИ ", + "profiles.logic.all": "ВСЕ", + "profiles.logic.any": "ЛЮБОЕ", + "profiles.updated": "Профиль обновлён", + "profiles.created": "Профиль создан", + "profiles.deleted": "Профиль удалён", + "profiles.error.name_required": "Введите название", + "time.hours_minutes": "{h}ч {m}м", + "time.minutes_seconds": "{m}м {s}с", + "time.seconds": "{s}с", + "dashboard.type.led": "LED", + "dashboard.type.kc": "Цвета клавиш", + "aria.close": "Закрыть", + "aria.save": "Сохранить", + "aria.cancel": "Отмена", + "aria.previous": "Назад", + "aria.next": "Вперёд", + "aria.hint": "Показать подсказку" } diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index abf95bd..0c0f81e 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -45,32 +45,32 @@
-
- - - - +
+ + + +
-
+
-
+
-
+
-
+
diff --git a/server/src/wled_controller/templates/modals/add-device.html b/server/src/wled_controller/templates/modals/add-device.html index 3a27959..8208f61 100644 --- a/server/src/wled_controller/templates/modals/add-device.html +++ b/server/src/wled_controller/templates/modals/add-device.html @@ -1,11 +1,11 @@ -