From 6388e0defa8ebf0667183541321c9dfa47edcf04 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 19 Feb 2026 12:32:14 +0300 Subject: [PATCH] Decouple i18n from feature modules and fix auth/login UX Replace hardcoded updateAllText() calls with languageChanged event pattern so feature modules subscribe independently. Guard all API calls behind apiKey checks to prevent unauthorized requests when not logged in. Fix login modal localization, hide tabs when logged out, clear all panels on logout, and treat profiles with no conditions as always-true. Co-Authored-By: Claude Opus 4.6 --- .../core/profiles/profile_engine.py | 4 +-- server/src/wled_controller/static/index.html | 27 ++++++++++++++----- server/src/wled_controller/static/js/app.js | 10 +++---- .../src/wled_controller/static/js/core/api.js | 21 ++++++++------- .../wled_controller/static/js/core/i18n.js | 16 ++++------- .../static/js/features/dashboard.js | 9 +++++++ .../static/js/features/profiles.js | 17 +++++++++--- .../static/js/features/streams.js | 4 +++ .../static/js/features/tabs.js | 5 ++-- .../static/js/features/targets.js | 4 +++ .../wled_controller/static/locales/en.json | 1 + .../wled_controller/static/locales/ru.json | 1 + server/src/wled_controller/static/style.css | 2 ++ 13 files changed, 81 insertions(+), 40 deletions(-) diff --git a/server/src/wled_controller/core/profiles/profile_engine.py b/server/src/wled_controller/core/profiles/profile_engine.py index a1048e7..7ef18f6 100644 --- a/server/src/wled_controller/core/profiles/profile_engine.py +++ b/server/src/wled_controller/core/profiles/profile_engine.py @@ -80,8 +80,8 @@ class ProfileEngine: for profile in profiles: should_be_active = ( profile.enabled - and len(profile.conditions) > 0 - and self._evaluate_conditions(profile, running_procs, topmost_proc) + and (len(profile.conditions) == 0 + or self._evaluate_conditions(profile, running_procs, topmost_proc)) ) is_active = profile.id in self._active_profiles diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index 6c7166a..2b6284d 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -1131,13 +1131,16 @@ const apiKey = localStorage.getItem('wled_api_key'); const loginBtn = document.getElementById('login-btn'); const logoutBtn = document.getElementById('logout-btn'); + const tabBar = document.querySelector('.tab-bar'); if (apiKey) { loginBtn.style.display = 'none'; logoutBtn.style.display = 'inline-block'; + if (tabBar) tabBar.style.display = ''; } else { loginBtn.style.display = 'inline-block'; logoutBtn.style.display = 'none'; + if (tabBar) tabBar.style.display = 'none'; } } @@ -1157,8 +1160,18 @@ updateAuthUI(); showToast(t('auth.logout.success'), 'info'); - // Clear the UI - document.getElementById('targets-panel-content').innerHTML = `
${t('auth.please_login')}
`; + // Stop background activity + if (window.stopDashboardWS) window.stopDashboardWS(); + if (window.stopPerfPolling) window.stopPerfPolling(); + if (window.stopUptimeTimer) window.stopUptimeTimer(); + if (window.disconnectAllKCWebSockets) window.disconnectAllKCWebSockets(); + + // Clear all tab panels + const loginMsg = `
${t('auth.please_login')}
`; + document.getElementById('dashboard-content').innerHTML = loginMsg; + document.getElementById('profiles-content').innerHTML = loginMsg; + document.getElementById('targets-panel-content').innerHTML = loginMsg; + document.getElementById('streams-list').innerHTML = loginMsg; } // Initialize on load @@ -1184,12 +1197,10 @@ const error = document.getElementById('api-key-error'); const cancelBtn = document.getElementById('modal-cancel-btn'); - if (message) { - description.textContent = message; - } + description.textContent = message || t('auth.message'); input.value = ''; - input.placeholder = 'Enter your API key...'; + input.placeholder = t('auth.placeholder'); error.style.display = 'none'; modal.style.display = 'flex'; lockBody(); @@ -1199,6 +1210,9 @@ const closeXBtn = document.getElementById('modal-close-x-btn'); if (closeXBtn) closeXBtn.style.display = hideCancel ? 'none' : ''; + // Hide login button while modal is open + document.getElementById('login-btn').style.display = 'none'; + setTimeout(() => input.focus(), 100); } @@ -1206,6 +1220,7 @@ const modal = document.getElementById('api-key-modal'); modal.style.display = 'none'; unlockBody(); + updateAuthUI(); } function submitApiKey(event) { diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index b6d5710..8049496 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -307,12 +307,12 @@ window.addEventListener('beforeunload', () => { // ─── Initialization ─── document.addEventListener('DOMContentLoaded', async () => { - // Initialize locale first - await initLocale(); - - // Load API key from localStorage + // Load API key from localStorage before anything that triggers API calls setApiKey(localStorage.getItem('wled_api_key')); + // Initialize locale (dispatches languageChanged which may trigger API calls) + await initLocale(); + // Restore active tab before showing content to avoid visible jump initTabs(); @@ -326,7 +326,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (!apiKey) { setTimeout(() => { if (typeof window.showApiKeyModal === 'function') { - window.showApiKeyModal('Welcome! Please login with your API key to get started.', true); + window.showApiKeyModal(null, true); } }, 100); return; diff --git a/server/src/wled_controller/static/js/core/api.js b/server/src/wled_controller/static/js/core/api.js index b755c4c..98b8467 100644 --- a/server/src/wled_controller/static/js/core/api.js +++ b/server/src/wled_controller/static/js/core/api.js @@ -48,18 +48,19 @@ export function handle401Error() { window.updateAuthUI(); } + const expiredMsg = typeof window.t === 'function' + ? window.t('auth.session_expired') + : 'Your session has expired. Please login again.'; + if (typeof window.showApiKeyModal === 'function') { - window.showApiKeyModal('Your session has expired or the API key is invalid. Please login again.', true); + window.showApiKeyModal(expiredMsg, true); } else { - // showToast imported at call sites to avoid circular dep - import('./state.js').then(() => { - const toast = document.getElementById('toast'); - if (toast) { - toast.textContent = 'Authentication failed. Please reload the page and login.'; - toast.className = 'toast error show'; - setTimeout(() => { toast.className = 'toast'; }, 3000); - } - }); + const toast = document.getElementById('toast'); + if (toast) { + toast.textContent = expiredMsg; + toast.className = 'toast error show'; + setTimeout(() => { toast.className = 'toast'; }, 3000); + } } } diff --git a/server/src/wled_controller/static/js/core/i18n.js b/server/src/wled_controller/static/js/core/i18n.js index b8a4e46..f7e1d4d 100644 --- a/server/src/wled_controller/static/js/core/i18n.js +++ b/server/src/wled_controller/static/js/core/i18n.js @@ -2,10 +2,9 @@ * Internationalization — translations, locale detection, text updates. */ -import { apiKey } from './state.js'; - let currentLocale = 'en'; let translations = {}; +let _initialized = false; const supportedLocales = { 'en': 'English', @@ -100,14 +99,9 @@ export function updateAllText() { el.title = t(key); }); - // Re-render dynamic content with new translations - if (apiKey) { - import('../core/api.js').then(({ loadDisplays }) => loadDisplays()); - if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); - if (typeof window.loadPictureSources === 'function') window.loadPictureSources(); - // Force perf section rebuild with new locale - const perfEl = document.querySelector('.dashboard-perf-persistent'); - if (perfEl) perfEl.remove(); - if (typeof window.loadDashboard === 'function') window.loadDashboard(); + // Notify subscribers that the language changed (skip initial load) + if (_initialized) { + document.dispatchEvent(new CustomEvent('languageChanged')); } + _initialized = true; } diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index c84e775..09b619f 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -593,6 +593,15 @@ export function stopUptimeTimer() { _stopUptimeTimer(); } +// Re-render dashboard when language changes +document.addEventListener('languageChanged', () => { + if (!apiKey) return; + // Force perf section rebuild with new locale + const perfEl = document.querySelector('.dashboard-perf-persistent'); + if (perfEl) perfEl.remove(); + loadDashboard(); +}); + export function stopDashboardWS() { if (_dashboardWS) { _dashboardWS.close(); diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/profiles.js index b6d92ad..9b917cb 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/profiles.js @@ -2,14 +2,17 @@ * Profiles — profile cards, editor, condition builder, process picker. */ -import { _profilesCache, set_profilesCache } from '../core/state.js'; +import { apiKey, _profilesCache, set_profilesCache } from '../core/state.js'; import { API_BASE, getHeaders, escapeHtml, handle401Error } from '../core/api.js'; -import { t, updateAllText } from '../core/i18n.js'; +import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js'; import { Modal } from '../core/modal.js'; const profileModal = new Modal('profile-editor-modal'); +// Re-render profiles when language changes +document.addEventListener('languageChanged', () => { if (apiKey) loadProfiles(); }); + export async function loadProfiles() { const container = document.getElementById('profiles-content'); if (!container) return; @@ -39,7 +42,11 @@ function renderProfiles(profiles) { html += ''; container.innerHTML = html; - updateAllText(); + // Localize data-i18n elements within the profiles container only + // (calling global updateAllText() would trigger loadProfiles() again → infinite loop) + container.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = t(el.getAttribute('data-i18n')); + }); } function createProfileCard(profile) { @@ -141,7 +148,9 @@ export async function openProfileEditor(profileId) { } profileModal.open(); - updateAllText(); + modal.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = t(el.getAttribute('data-i18n')); + }); } export function closeProfileEditorModal() { diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index ed15797..09b3308 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -18,6 +18,7 @@ import { _currentTestStreamId, set_currentTestStreamId, _currentTestPPTemplateId, set_currentTestPPTemplateId, _lastValidatedImageSource, set_lastValidatedImageSource, + apiKey, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -25,6 +26,9 @@ import { Modal } from '../core/modal.js'; import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js'; import { openDisplayPicker, formatDisplayLabel } from './displays.js'; +// Re-render picture sources when language changes +document.addEventListener('languageChanged', () => { if (apiKey) loadPictureSources(); }); + // ===== Modal instances ===== const templateModal = new Modal('template-modal'); diff --git a/server/src/wled_controller/static/js/features/tabs.js b/server/src/wled_controller/static/js/features/tabs.js index 88fa1f0..055d4b9 100644 --- a/server/src/wled_controller/static/js/features/tabs.js +++ b/server/src/wled_controller/static/js/features/tabs.js @@ -10,12 +10,13 @@ export function switchTab(name) { localStorage.setItem('activeTab', name); if (name === 'dashboard') { // Use window.* to avoid circular imports with feature modules - if (typeof window.loadDashboard === 'function') window.loadDashboard(); - if (typeof window.startDashboardWS === 'function') window.startDashboardWS(); + if (apiKey && typeof window.loadDashboard === 'function') window.loadDashboard(); + if (apiKey && typeof window.startDashboardWS === 'function') window.startDashboardWS(); } else { if (typeof window.stopDashboardWS === 'function') window.stopDashboardWS(); if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling(); if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer(); + if (!apiKey) return; if (name === 'streams') { if (typeof window.loadPictureSources === 'function') window.loadPictureSources(); } else if (name === 'targets') { diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index ef09e4f..bc80b88 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -3,6 +3,7 @@ */ import { + apiKey, _targetEditorDevices, set_targetEditorDevices, _deviceBrightnessCache, kcWebSockets, @@ -17,6 +18,9 @@ import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from '. // createPatternTemplateCard is imported via window.* to avoid circular deps // (pattern-templates.js calls window.loadTargetsTab) +// Re-render targets tab when language changes +document.addEventListener('languageChanged', () => { if (apiKey) loadTargetsTab(); }); + class TargetEditorModal extends Modal { constructor() { super('target-editor-modal'); diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 587bb68..7715df6 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -19,6 +19,7 @@ "auth.logout.confirm": "Are you sure you want to logout?", "auth.logout.success": "Logged out successfully", "auth.please_login": "Please login to view", + "auth.session_expired": "Your session has expired or the API key is invalid. Please login again.", "displays.title": "Available Displays", "displays.layout": "\uD83D\uDDA5\uFE0F Displays", "displays.information": "Display Information", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 2eb37fb..d0468ea 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -19,6 +19,7 @@ "auth.logout.confirm": "Вы уверены, что хотите выйти?", "auth.logout.success": "Выход выполнен успешно", "auth.please_login": "Пожалуйста, войдите для просмотра", + "auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.", "displays.title": "Доступные Дисплеи", "displays.layout": "\uD83D\uDDA5\uFE0F Дисплеи", "displays.information": "Информация о Дисплеях", diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index 5141cde..092d459 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -66,6 +66,8 @@ header { align-items: center; padding: 20px 0 10px; margin-bottom: 10px; + position: relative; + z-index: 2100; } .header-title {