From 58df163dedbc122f4981104391f02bc501a894d0 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 12 Feb 2026 15:44:11 +0300 Subject: [PATCH] Rename WLED Grab to LED Grab, merge Devices into Targets tab with WLED sub-tab, and UI polish - Rename "WLED Grab" to "LED Grab" across all files (title, logs, locales) - Merge Devices and Targets into a single Targets tab with WLED sub-tab containing Devices and Targets sections (like Sources tab pattern) - Make target card source name label full-width - Render engine template config as two-column key-value grid - Update CLAUDE.md: no server restart needed for frontend-only changes Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 +- server/src/wled_controller/__init__.py | 2 +- server/src/wled_controller/config.py | 2 +- server/src/wled_controller/main.py | 8 +- server/src/wled_controller/static/app.js | 237 +++++++++--------- server/src/wled_controller/static/index.html | 23 +- .../wled_controller/static/locales/en.json | 7 +- .../wled_controller/static/locales/ru.json | 9 +- server/src/wled_controller/static/style.css | 26 ++ 9 files changed, 176 insertions(+), 142 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ab1606f..b674c9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,9 @@ ## IMPORTANT: Auto-Restart Server on Code Changes -**Whenever server-side Python code is modified** (any file under `/server/src/`), **automatically restart the server** so the changes take effect immediately. Do NOT wait for the user to ask for a restart. +**Whenever server-side Python code is modified** (any file under `/server/src/` **excluding** `/server/src/wled_controller/static/`), **automatically restart the server** so the changes take effect immediately. Do NOT wait for the user to ask for a restart. + +**No restart needed for frontend-only changes.** Files under `/server/src/wled_controller/static/` (HTML, JS, CSS, JSON locale files) are served directly by FastAPI's static file handler — changes take effect on the next browser page refresh without restarting the server. ### Restart procedure diff --git a/server/src/wled_controller/__init__.py b/server/src/wled_controller/__init__.py index 128f359..9d2de11 100644 --- a/server/src/wled_controller/__init__.py +++ b/server/src/wled_controller/__init__.py @@ -1,4 +1,4 @@ -"""WLED Grab - Ambient lighting based on screen content.""" +"""LED Grab - Ambient lighting based on screen content.""" __version__ = "0.1.0" __author__ = "Alexei Dolgolyov" diff --git a/server/src/wled_controller/config.py b/server/src/wled_controller/config.py index c75d5e7..87333ac 100644 --- a/server/src/wled_controller/config.py +++ b/server/src/wled_controller/config.py @@ -1,4 +1,4 @@ -"""Configuration management for WLED Grab.""" +"""Configuration management for LED Grab.""" import os from pathlib import Path diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index a9e1001..d38ce66 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -107,7 +107,7 @@ async def lifespan(app: FastAPI): Handles startup and shutdown events. """ # Startup - logger.info(f"Starting WLED Grab v{__version__}") + logger.info(f"Starting LED Grab v{__version__}") logger.info(f"Python version: {sys.version}") logger.info(f"Server listening on {config.server.host}:{config.server.port}") @@ -181,7 +181,7 @@ async def lifespan(app: FastAPI): yield # Shutdown - logger.info("Shutting down WLED Grab") + logger.info("Shutting down LED Grab") # Stop all processing try: @@ -192,7 +192,7 @@ async def lifespan(app: FastAPI): # Create FastAPI application app = FastAPI( - title="WLED Grab", + title="LED Grab", description="Control WLED devices based on screen content for ambient lighting", version=__version__, lifespan=lifespan, @@ -246,7 +246,7 @@ async def root(): # Fallback to JSON if static files not found return { - "name": "WLED Grab", + "name": "LED Grab", "version": __version__, "docs": "/docs", "health": "/health", diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index de9aabd..31b60fa 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -288,7 +288,7 @@ const supportedLocales = { // Minimal inline fallback for critical UI elements const fallbackTranslations = { - 'app.title': 'WLED Grab', + 'app.title': 'LED Grab', 'auth.placeholder': 'Enter your API key...', 'auth.button.login': 'Login' }; @@ -400,7 +400,7 @@ function updateAllText() { // Re-render dynamic content with new translations if (apiKey) { loadDisplays(); - loadDevices(); + loadTargetsTab(); loadPictureSources(); } } @@ -436,7 +436,7 @@ document.addEventListener('DOMContentLoaded', async () => { // User is logged in, load data loadServerInfo(); loadDisplays(); - loadDevices(); + loadTargetsTab(); // Start auto-refresh startAutoRefresh(); @@ -581,12 +581,14 @@ function switchTab(name) { if (name === 'streams') { loadPictureSources(); } else if (name === 'targets') { - loadTargets(); + loadTargetsTab(); } } function initTabs() { - const saved = localStorage.getItem('activeTab'); + let saved = localStorage.getItem('activeTab'); + // Migrate legacy 'devices' tab to 'targets' (devices now live inside targets) + if (saved === 'devices') saved = 'targets'; if (saved && document.getElementById(`tab-${saved}`)) { switchTab(saved); } @@ -595,68 +597,8 @@ function initTabs() { // Load devices async function loadDevices() { - try { - const response = await fetch(`${API_BASE}/devices`, { - headers: getHeaders() - }); - - if (response.status === 401) { - handle401Error(); - return; - } - - const data = await response.json(); - const devices = data.devices || []; - - const container = document.getElementById('devices-list'); - - if (!devices || devices.length === 0) { - container.innerHTML = `
-
+
-
`; - return; - } - - // Fetch state for each device - const devicesWithState = await Promise.all( - devices.map(async (device) => { - try { - const stateResponse = await fetch(`${API_BASE}/devices/${device.id}/state`, { - headers: getHeaders() - }); - const state = stateResponse.ok ? await stateResponse.json() : {}; - return { ...device, state }; - } catch (error) { - console.error(`Failed to load state for device ${device.id}:`, error); - return device; - } - }) - ); - - container.innerHTML = devicesWithState.map(device => createDeviceCard(device)).join('') - + `
-
+
-
${t('devices.add')}
-
`; - - // Update footer WLED Web UI link with first device's URL - const webuiLink = document.querySelector('.wled-webui-link'); - if (webuiLink && devicesWithState.length > 0 && devicesWithState[0].url) { - webuiLink.href = devicesWithState[0].url; - webuiLink.target = '_blank'; - webuiLink.rel = 'noopener'; - } - - // Attach event listeners and fetch real WLED brightness - devicesWithState.forEach(device => { - attachDeviceListeners(device.id); - fetchDeviceBrightness(device.id); - }); - } catch (error) { - console.error('Failed to load devices:', error); - document.getElementById('devices-list').innerHTML = - `
${t('devices.failed')}
`; - } + // Devices now render inside the combined Targets tab + await loadTargetsTab(); } function createDeviceCard(device) { @@ -1000,11 +942,9 @@ function startAutoRefresh() { refreshInterval = setInterval(() => { // Only refresh if user is authenticated if (apiKey) { - const activeTab = localStorage.getItem('activeTab') || 'devices'; - if (activeTab === 'devices') { - loadDevices(); - } else if (activeTab === 'targets') { - loadTargets(); + const activeTab = localStorage.getItem('activeTab') || 'targets'; + if (activeTab === 'targets') { + loadTargetsTab(); } } }, 2000); // Refresh every 2 seconds @@ -2514,13 +2454,14 @@ async function onEngineChange() { configSection.style.display = 'none'; return; } else { + let gridHtml = '
'; Object.entries(defaultConfig).forEach(([key, value]) => { const fieldType = typeof value === 'number' ? 'number' : 'text'; const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value; - const fieldHtml = ` -
- + gridHtml += ` + +
${typeof value === 'boolean' ? ` `} - ${t('templates.config.default')}: ${JSON.stringify(value)}
`; - configFields.innerHTML += fieldHtml; }); + gridHtml += '
'; + configFields.innerHTML = gridHtml; } configSection.style.display = 'block'; @@ -3998,27 +3939,66 @@ async function saveTargetEditor() { } } -// ===== PICTURE TARGETS ===== +// ===== TARGETS TAB (WLED devices + targets combined) ===== async function loadTargets() { + // Alias for backward compatibility + await loadTargetsTab(); +} + +function switchTargetSubTab(tabKey) { + document.querySelectorAll('.target-sub-tab-btn').forEach(btn => + btn.classList.toggle('active', btn.dataset.targetSubTab === tabKey) + ); + document.querySelectorAll('.target-sub-tab-panel').forEach(panel => + panel.classList.toggle('active', panel.id === `target-sub-tab-${tabKey}`) + ); + localStorage.setItem('activeTargetSubTab', tabKey); +} + +async function loadTargetsTab() { + const container = document.getElementById('targets-panel-content'); + if (!container) return; + try { - const response = await fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() }); + // Fetch devices, targets, and sources in parallel + const [devicesResp, targetsResp, sourcesResp] = await Promise.all([ + fetch(`${API_BASE}/devices`, { headers: getHeaders() }), + fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() }), + fetchWithAuth('/picture-sources').catch(() => null), + ]); - if (response.status === 401) { handle401Error(); return; } - - const data = await response.json(); - const targets = data.targets || []; - - const container = document.getElementById('targets-list'); - - if (!targets || targets.length === 0) { - container.innerHTML = `
-
+
-
`; + if (devicesResp.status === 401 || targetsResp.status === 401) { + handle401Error(); return; } - // Fetch state for each target + const devicesData = await devicesResp.json(); + const devices = devicesData.devices || []; + + const targetsData = await targetsResp.json(); + const targets = targetsData.targets || []; + + let sourceMap = {}; + if (sourcesResp && sourcesResp.ok) { + const srcData = await sourcesResp.json(); + (srcData.streams || []).forEach(s => { sourceMap[s.id] = s; }); + } + + // Fetch state for each device + const devicesWithState = await Promise.all( + devices.map(async (device) => { + try { + const stateResp = await fetch(`${API_BASE}/devices/${device.id}/state`, { headers: getHeaders() }); + const state = stateResp.ok ? await stateResp.json() : {}; + return { ...device, state }; + } catch { + return device; + } + }) + ); + + // Fetch state + metrics for each target const targetsWithState = await Promise.all( targets.map(async (target) => { try { @@ -4033,31 +4013,58 @@ async function loadTargets() { }) ); - // Also fetch devices and sources for name resolution - let deviceMap = {}; - let sourceMap = {}; - try { - const devResp = await fetch(`${API_BASE}/devices`, { headers: getHeaders() }); - if (devResp.ok) { - const devData = await devResp.json(); - (devData.devices || []).forEach(d => { deviceMap[d.id] = d; }); - } - } catch {} - try { - const srcResp = await fetchWithAuth('/picture-sources'); - if (srcResp.ok) { - const srcData = await srcResp.json(); - (srcData.streams || []).forEach(s => { sourceMap[s.id] = s; }); - } - } catch {} + // Build device map for target name resolution + const deviceMap = {}; + devicesWithState.forEach(d => { deviceMap[d.id] = d; }); + + // Group by type (currently only WLED) + const wledDevices = devicesWithState; + const wledTargets = targetsWithState.filter(t => t.target_type === 'wled'); + + const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'wled'; + + const subTabs = [ + { key: 'wled', icon: '💡', titleKey: 'targets.subtab.wled', count: wledDevices.length + wledTargets.length }, + ]; + + const tabBar = `
${subTabs.map(tab => + `` + ).join('')}
`; + + // WLED panel: devices section + targets section + const wledPanel = ` +
+
+

${t('targets.section.devices')}

+
+ ${wledDevices.map(device => createDeviceCard(device)).join('')} +
+
+
+
+
+
+
+

${t('targets.section.targets')}

+
+ ${wledTargets.map(target => createTargetCard(target, deviceMap, sourceMap)).join('')} +
+
+
+
+
+
+
`; + + container.innerHTML = tabBar + wledPanel; + + // Attach event listeners and fetch WLED brightness for device cards + devicesWithState.forEach(device => { + attachDeviceListeners(device.id); + fetchDeviceBrightness(device.id); + }); - container.innerHTML = targetsWithState.map(target => createTargetCard(target, deviceMap, sourceMap)).join('') - + `
-
+
-
`; } catch (error) { - console.error('Failed to load targets:', error); - document.getElementById('targets-list').innerHTML = `
${t('targets.failed')}
`; + console.error('Failed to load targets tab:', error); + container.innerHTML = `
${t('targets.failed')}
`; } } @@ -4095,7 +4102,7 @@ function createTargetCard(target, deviceMap, sourceMap) {
💡 ${escapeHtml(deviceName)} - 📺 ${escapeHtml(sourceName)} + 📺 ${escapeHtml(sourceName)}
${isProcessing ? ` diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index e02c2cf..023f61f 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -3,7 +3,7 @@ - WLED Grab + LED Grab @@ -12,7 +12,7 @@
-

WLED Grab

+

LED Grab

@@ -34,19 +34,12 @@
- + -
-
-
-
-
-
- -
-
+
+
@@ -321,7 +314,7 @@