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 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 12:32:14 +03:00
parent 747cdfabd6
commit 6388e0defa
13 changed files with 81 additions and 40 deletions

View File

@@ -80,8 +80,8 @@ class ProfileEngine:
for profile in profiles: for profile in profiles:
should_be_active = ( should_be_active = (
profile.enabled profile.enabled
and len(profile.conditions) > 0 and (len(profile.conditions) == 0
and self._evaluate_conditions(profile, running_procs, topmost_proc) or self._evaluate_conditions(profile, running_procs, topmost_proc))
) )
is_active = profile.id in self._active_profiles is_active = profile.id in self._active_profiles

View File

@@ -1131,13 +1131,16 @@
const apiKey = localStorage.getItem('wled_api_key'); const apiKey = localStorage.getItem('wled_api_key');
const loginBtn = document.getElementById('login-btn'); const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn'); const logoutBtn = document.getElementById('logout-btn');
const tabBar = document.querySelector('.tab-bar');
if (apiKey) { if (apiKey) {
loginBtn.style.display = 'none'; loginBtn.style.display = 'none';
logoutBtn.style.display = 'inline-block'; logoutBtn.style.display = 'inline-block';
if (tabBar) tabBar.style.display = '';
} else { } else {
loginBtn.style.display = 'inline-block'; loginBtn.style.display = 'inline-block';
logoutBtn.style.display = 'none'; logoutBtn.style.display = 'none';
if (tabBar) tabBar.style.display = 'none';
} }
} }
@@ -1157,8 +1160,18 @@
updateAuthUI(); updateAuthUI();
showToast(t('auth.logout.success'), 'info'); showToast(t('auth.logout.success'), 'info');
// Clear the UI // Stop background activity
document.getElementById('targets-panel-content').innerHTML = `<div class="loading">${t('auth.please_login')}</div>`; 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 = `<div class="loading">${t('auth.please_login')}</div>`;
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 // Initialize on load
@@ -1184,12 +1197,10 @@
const error = document.getElementById('api-key-error'); const error = document.getElementById('api-key-error');
const cancelBtn = document.getElementById('modal-cancel-btn'); const cancelBtn = document.getElementById('modal-cancel-btn');
if (message) { description.textContent = message || t('auth.message');
description.textContent = message;
}
input.value = ''; input.value = '';
input.placeholder = 'Enter your API key...'; input.placeholder = t('auth.placeholder');
error.style.display = 'none'; error.style.display = 'none';
modal.style.display = 'flex'; modal.style.display = 'flex';
lockBody(); lockBody();
@@ -1199,6 +1210,9 @@
const closeXBtn = document.getElementById('modal-close-x-btn'); const closeXBtn = document.getElementById('modal-close-x-btn');
if (closeXBtn) closeXBtn.style.display = hideCancel ? 'none' : ''; 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); setTimeout(() => input.focus(), 100);
} }
@@ -1206,6 +1220,7 @@
const modal = document.getElementById('api-key-modal'); const modal = document.getElementById('api-key-modal');
modal.style.display = 'none'; modal.style.display = 'none';
unlockBody(); unlockBody();
updateAuthUI();
} }
function submitApiKey(event) { function submitApiKey(event) {

View File

@@ -307,12 +307,12 @@ window.addEventListener('beforeunload', () => {
// ─── Initialization ─── // ─── Initialization ───
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
// Initialize locale first // Load API key from localStorage before anything that triggers API calls
await initLocale();
// Load API key from localStorage
setApiKey(localStorage.getItem('wled_api_key')); 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 // Restore active tab before showing content to avoid visible jump
initTabs(); initTabs();
@@ -326,7 +326,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!apiKey) { if (!apiKey) {
setTimeout(() => { setTimeout(() => {
if (typeof window.showApiKeyModal === 'function') { if (typeof window.showApiKeyModal === 'function') {
window.showApiKeyModal('Welcome! Please login with your API key to get started.', true); window.showApiKeyModal(null, true);
} }
}, 100); }, 100);
return; return;

View File

@@ -48,18 +48,19 @@ export function handle401Error() {
window.updateAuthUI(); 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') { 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 { } else {
// showToast imported at call sites to avoid circular dep const toast = document.getElementById('toast');
import('./state.js').then(() => { if (toast) {
const toast = document.getElementById('toast'); toast.textContent = expiredMsg;
if (toast) { toast.className = 'toast error show';
toast.textContent = 'Authentication failed. Please reload the page and login.'; setTimeout(() => { toast.className = 'toast'; }, 3000);
toast.className = 'toast error show'; }
setTimeout(() => { toast.className = 'toast'; }, 3000);
}
});
} }
} }

View File

@@ -2,10 +2,9 @@
* Internationalization — translations, locale detection, text updates. * Internationalization — translations, locale detection, text updates.
*/ */
import { apiKey } from './state.js';
let currentLocale = 'en'; let currentLocale = 'en';
let translations = {}; let translations = {};
let _initialized = false;
const supportedLocales = { const supportedLocales = {
'en': 'English', 'en': 'English',
@@ -100,14 +99,9 @@ export function updateAllText() {
el.title = t(key); el.title = t(key);
}); });
// Re-render dynamic content with new translations // Notify subscribers that the language changed (skip initial load)
if (apiKey) { if (_initialized) {
import('../core/api.js').then(({ loadDisplays }) => loadDisplays()); document.dispatchEvent(new CustomEvent('languageChanged'));
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();
} }
_initialized = true;
} }

View File

@@ -593,6 +593,15 @@ export function stopUptimeTimer() {
_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() { export function stopDashboardWS() {
if (_dashboardWS) { if (_dashboardWS) {
_dashboardWS.close(); _dashboardWS.close();

View File

@@ -2,14 +2,17 @@
* Profiles — profile cards, editor, condition builder, process picker. * 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 { 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 { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js'; import { Modal } from '../core/modal.js';
const profileModal = new Modal('profile-editor-modal'); const profileModal = new Modal('profile-editor-modal');
// Re-render profiles when language changes
document.addEventListener('languageChanged', () => { if (apiKey) loadProfiles(); });
export async function loadProfiles() { export async function loadProfiles() {
const container = document.getElementById('profiles-content'); const container = document.getElementById('profiles-content');
if (!container) return; if (!container) return;
@@ -39,7 +42,11 @@ function renderProfiles(profiles) {
html += '</div>'; html += '</div>';
container.innerHTML = 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) { function createProfileCard(profile) {
@@ -141,7 +148,9 @@ export async function openProfileEditor(profileId) {
} }
profileModal.open(); profileModal.open();
updateAllText(); modal.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
} }
export function closeProfileEditorModal() { export function closeProfileEditorModal() {

View File

@@ -18,6 +18,7 @@ import {
_currentTestStreamId, set_currentTestStreamId, _currentTestStreamId, set_currentTestStreamId,
_currentTestPPTemplateId, set_currentTestPPTemplateId, _currentTestPPTemplateId, set_currentTestPPTemplateId,
_lastValidatedImageSource, set_lastValidatedImageSource, _lastValidatedImageSource, set_lastValidatedImageSource,
apiKey,
} from '../core/state.js'; } from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.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 { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js';
import { openDisplayPicker, formatDisplayLabel } from './displays.js'; import { openDisplayPicker, formatDisplayLabel } from './displays.js';
// Re-render picture sources when language changes
document.addEventListener('languageChanged', () => { if (apiKey) loadPictureSources(); });
// ===== Modal instances ===== // ===== Modal instances =====
const templateModal = new Modal('template-modal'); const templateModal = new Modal('template-modal');

View File

@@ -10,12 +10,13 @@ export function switchTab(name) {
localStorage.setItem('activeTab', name); localStorage.setItem('activeTab', name);
if (name === 'dashboard') { if (name === 'dashboard') {
// Use window.* to avoid circular imports with feature modules // Use window.* to avoid circular imports with feature modules
if (typeof window.loadDashboard === 'function') window.loadDashboard(); if (apiKey && typeof window.loadDashboard === 'function') window.loadDashboard();
if (typeof window.startDashboardWS === 'function') window.startDashboardWS(); if (apiKey && typeof window.startDashboardWS === 'function') window.startDashboardWS();
} else { } else {
if (typeof window.stopDashboardWS === 'function') window.stopDashboardWS(); if (typeof window.stopDashboardWS === 'function') window.stopDashboardWS();
if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling(); if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling();
if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer(); if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer();
if (!apiKey) return;
if (name === 'streams') { if (name === 'streams') {
if (typeof window.loadPictureSources === 'function') window.loadPictureSources(); if (typeof window.loadPictureSources === 'function') window.loadPictureSources();
} else if (name === 'targets') { } else if (name === 'targets') {

View File

@@ -3,6 +3,7 @@
*/ */
import { import {
apiKey,
_targetEditorDevices, set_targetEditorDevices, _targetEditorDevices, set_targetEditorDevices,
_deviceBrightnessCache, _deviceBrightnessCache,
kcWebSockets, kcWebSockets,
@@ -17,6 +18,9 @@ import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from '.
// createPatternTemplateCard is imported via window.* to avoid circular deps // createPatternTemplateCard is imported via window.* to avoid circular deps
// (pattern-templates.js calls window.loadTargetsTab) // (pattern-templates.js calls window.loadTargetsTab)
// Re-render targets tab when language changes
document.addEventListener('languageChanged', () => { if (apiKey) loadTargetsTab(); });
class TargetEditorModal extends Modal { class TargetEditorModal extends Modal {
constructor() { constructor() {
super('target-editor-modal'); super('target-editor-modal');

View File

@@ -19,6 +19,7 @@
"auth.logout.confirm": "Are you sure you want to logout?", "auth.logout.confirm": "Are you sure you want to logout?",
"auth.logout.success": "Logged out successfully", "auth.logout.success": "Logged out successfully",
"auth.please_login": "Please login to view", "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.title": "Available Displays",
"displays.layout": "\uD83D\uDDA5\uFE0F Displays", "displays.layout": "\uD83D\uDDA5\uFE0F Displays",
"displays.information": "Display Information", "displays.information": "Display Information",

View File

@@ -19,6 +19,7 @@
"auth.logout.confirm": "Вы уверены, что хотите выйти?", "auth.logout.confirm": "Вы уверены, что хотите выйти?",
"auth.logout.success": "Выход выполнен успешно", "auth.logout.success": "Выход выполнен успешно",
"auth.please_login": "Пожалуйста, войдите для просмотра", "auth.please_login": "Пожалуйста, войдите для просмотра",
"auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.",
"displays.title": "Доступные Дисплеи", "displays.title": "Доступные Дисплеи",
"displays.layout": "\uD83D\uDDA5\uFE0F Дисплеи", "displays.layout": "\uD83D\uDDA5\uFE0F Дисплеи",
"displays.information": "Информация о Дисплеях", "displays.information": "Информация о Дисплеях",

View File

@@ -66,6 +66,8 @@ header {
align-items: center; align-items: center;
padding: 20px 0 10px; padding: 20px 0 10px;
margin-bottom: 10px; margin-bottom: 10px;
position: relative;
z-index: 2100;
} }
.header-title { .header-title {