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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = `<div class="template-card add-template-card" onclick="showAddDevice()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>`;
|
||||
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('')
|
||||
+ `<div class="card add-device-card" onclick="showAddDevice()">
|
||||
<div class="add-device-icon">+</div>
|
||||
<div class="add-device-label">${t('devices.add')}</div>
|
||||
</div>`;
|
||||
|
||||
// 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 =
|
||||
`<div class="loading">${t('devices.failed')}</div>`;
|
||||
}
|
||||
// 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 = '<div class="config-grid">';
|
||||
Object.entries(defaultConfig).forEach(([key, value]) => {
|
||||
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
||||
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
||||
|
||||
const fieldHtml = `
|
||||
<div class="form-group">
|
||||
<label for="config-${key}">${key}:</label>
|
||||
gridHtml += `
|
||||
<label class="config-grid-label" for="config-${key}">${key}</label>
|
||||
<div class="config-grid-value">
|
||||
${typeof value === 'boolean' ? `
|
||||
<select id="config-${key}" data-config-key="${key}">
|
||||
<option value="true" ${value ? 'selected' : ''}>true</option>
|
||||
@@ -2529,11 +2470,11 @@ async function onEngineChange() {
|
||||
` : `
|
||||
<input type="${fieldType}" id="config-${key}" data-config-key="${key}" value="${fieldValue}">
|
||||
`}
|
||||
<small class="form-hint">${t('templates.config.default')}: ${JSON.stringify(value)}</small>
|
||||
</div>
|
||||
`;
|
||||
configFields.innerHTML += fieldHtml;
|
||||
});
|
||||
gridHtml += '</div>';
|
||||
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 = `<div class="template-card add-template-card" onclick="showTargetEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>`;
|
||||
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 = `<div class="stream-tab-bar">${subTabs.map(tab =>
|
||||
`<button class="target-sub-tab-btn stream-tab-btn${tab.key === activeSubTab ? ' active' : ''}" data-target-sub-tab="${tab.key}" onclick="switchTargetSubTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.count}</span></button>`
|
||||
).join('')}</div>`;
|
||||
|
||||
// WLED panel: devices section + targets section
|
||||
const wledPanel = `
|
||||
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'wled' ? ' active' : ''}" id="target-sub-tab-wled">
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.devices')}</h3>
|
||||
<div class="devices-grid">
|
||||
${wledDevices.map(device => createDeviceCard(device)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddDevice()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.targets')}</h3>
|
||||
<div class="devices-grid">
|
||||
${wledTargets.map(target => createTargetCard(target, deviceMap, sourceMap)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showTargetEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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('')
|
||||
+ `<div class="template-card add-template-card" onclick="showTargetEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load targets:', error);
|
||||
document.getElementById('targets-list').innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
||||
console.error('Failed to load targets tab:', error);
|
||||
container.innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4095,7 +4102,7 @@ function createTargetCard(target, deviceMap, sourceMap) {
|
||||
</div>
|
||||
<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('targets.device')}">💡 ${escapeHtml(deviceName)}</span>
|
||||
<span class="stream-card-prop" title="${t('targets.source')}">📺 ${escapeHtml(sourceName)}</span>
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${t('targets.source')}">📺 ${escapeHtml(sourceName)}</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${isProcessing ? `
|
||||
|
||||
Reference in New Issue
Block a user