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 = `
-
';
+ 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 = `
+
+
+
+
+ ${wledDevices.map(device => createDeviceCard(device)).join('')}
+
+
+
+
+
+
+ ${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 @@
@@ -34,19 +34,12 @@
-
+
-
-
-
-