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 {