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:
@@ -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
|
||||
|
||||
@@ -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 = `<div class="loading">${t('auth.please_login')}</div>`;
|
||||
// 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 = `<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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 += '</div>';
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Информация о Дисплеях",
|
||||
|
||||
@@ -66,6 +66,8 @@ header {
|
||||
align-items: center;
|
||||
padding: 20px 0 10px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: 2100;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
|
||||
Reference in New Issue
Block a user