diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css
index 8cddbca..9cc69cb 100644
--- a/server/src/wled_controller/static/css/dashboard.css
+++ b/server/src/wled_controller/static/css/dashboard.css
@@ -218,6 +218,21 @@
min-width: 48px;
}
+.dashboard-autostart-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 4px;
+}
+
+.dashboard-autostart {
+ grid-template-columns: 1fr auto;
+ margin-bottom: 0;
+}
+
+.dashboard-autostart .dashboard-target-info > div {
+ min-width: 0;
+}
+
@media (max-width: 768px) {
.dashboard-target {
grid-template-columns: 1fr auto;
diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js
index 267a466..34d09ca 100644
--- a/server/src/wled_controller/static/js/app.js
+++ b/server/src/wled_controller/static/js/app.js
@@ -90,6 +90,7 @@ import {
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
expandAllTargetSections, collapseAllTargetSections,
+ disconnectAllLedPreviewWS,
} from './features/targets.js';
// Layer 5: color-strip sources
@@ -303,6 +304,7 @@ Object.assign(window, {
cloneTarget,
toggleLedPreview,
toggleTargetAutoStart,
+ disconnectAllLedPreviewWS,
// color-strip sources
showCSSEditor,
@@ -413,6 +415,7 @@ window.addEventListener('beforeunload', () => {
}
stopEventsWS();
disconnectAllKCWebSockets();
+ disconnectAllLedPreviewWS();
});
// ─── Initialization ───
diff --git a/server/src/wled_controller/static/js/core/state.js b/server/src/wled_controller/static/js/core/state.js
index 94144cc..12ddadf 100644
--- a/server/src/wled_controller/static/js/core/state.js
+++ b/server/src/wled_controller/static/js/core/state.js
@@ -121,10 +121,13 @@ export function setActiveTutorial(v) { activeTutorial = v; }
export let confirmResolve = null;
export function setConfirmResolve(v) { confirmResolve = v; }
-// Dashboard loading guard
+// Loading guards
export let _dashboardLoading = false;
export function set_dashboardLoading(v) { _dashboardLoading = v; }
+export let _sourcesLoading = false;
+export function set_sourcesLoading(v) { _sourcesLoading = v; }
+
// Dashboard poll interval (ms), persisted in localStorage
const _POLL_KEY = 'dashboard_poll_interval';
const _POLL_DEFAULT = 2000;
diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js
index 654b452..ee95075 100644
--- a/server/src/wled_controller/static/js/features/dashboard.js
+++ b/server/src/wled_controller/static/js/features/dashboard.js
@@ -370,23 +370,32 @@ export async function loadDashboard(forceFullRender = false) {
const autoStartTargets = enriched.filter(t => t.auto_start);
if (autoStartTargets.length > 0) {
- const autoStartItems = autoStartTargets.map(target => {
+ const autoStartCards = autoStartTargets.map(target => {
const isRunning = !!(target.state && target.state.processing);
- const device = devicesMap[target.device_id];
- const deviceName = device ? device.name : '';
- const typeIcon = getTargetTypeIcon(target.target_type);
+ const isLed = target.target_type !== 'key_colors';
+ const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
+ const subtitleParts = [typeLabel];
+ if (isLed) {
+ const device = target.device_id ? devicesMap[target.device_id] : null;
+ if (device) subtitleParts.push((device.device_type || '').toUpperCase());
+ const cssId = target.color_strip_source_id || '';
+ if (cssId) {
+ const css = cssSourceMap[cssId];
+ if (css) subtitleParts.push(t(`color_strip.type.${css.source_type}`) || css.source_type);
+ }
+ }
const statusBadge = isRunning
? `${t('profiles.status.active')}`
: `${t('profiles.status.inactive')}`;
+ const subtitle = subtitleParts.length ? `
${escapeHtml(subtitleParts.join(' · '))}
` : '';
return `
${ICON_AUTOSTART}
${escapeHtml(target.name)} ${statusBadge}
- ${deviceName ? `
${typeIcon} ${escapeHtml(deviceName)}
` : `
${typeIcon}
`}
+ ${subtitle}
-
`;
}).join('');
+ const autoStartItems = `${autoStartCards}
`;
dynamicHtml += `
${_sectionHeader('autostart', t('autostart.title'), autoStartTargets.length)}
@@ -707,3 +717,13 @@ document.addEventListener('languageChanged', () => {
if (perfEl) perfEl.remove();
loadDashboard();
});
+
+// Pause uptime timer when browser tab is hidden, resume when visible
+document.addEventListener('visibilitychange', () => {
+ if (document.hidden) {
+ _stopUptimeTimer();
+ } else if (_isDashboardActive() && _lastRunningIds.length > 0) {
+ _cacheUptimeElements();
+ _startUptimeTimer();
+ }
+});
diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js
index ba786a5..ed37bcb 100644
--- a/server/src/wled_controller/static/js/features/kc-targets.js
+++ b/server/src/wled_controller/static/js/features/kc-targets.js
@@ -607,6 +607,7 @@ export async function saveKCBrightness(targetId, value) {
});
} catch (err) {
console.error('Failed to save KC brightness:', err);
+ showToast(t('kc.error.brightness') || 'Failed to save brightness', 'error');
}
}
diff --git a/server/src/wled_controller/static/js/features/perf-charts.js b/server/src/wled_controller/static/js/features/perf-charts.js
index d352a94..51d8f30 100644
--- a/server/src/wled_controller/static/js/features/perf-charts.js
+++ b/server/src/wled_controller/static/js/features/perf-charts.js
@@ -180,3 +180,14 @@ export function stopPerfPolling() {
_pollTimer = null;
}
}
+
+// Pause polling when browser tab becomes hidden, resume when visible
+document.addEventListener('visibilitychange', () => {
+ if (document.hidden) {
+ stopPerfPolling();
+ } else {
+ // Only resume if dashboard is active
+ const activeTab = localStorage.getItem('activeTab') || 'dashboard';
+ if (activeTab === 'dashboard') startPerfPolling();
+ }
+});
diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js
index a334361..0a951a5 100644
--- a/server/src/wled_controller/static/js/features/streams.js
+++ b/server/src/wled_controller/static/js/features/streams.js
@@ -20,6 +20,7 @@ import {
_lastValidatedImageSource, set_lastValidatedImageSource,
_cachedAudioSources, set_cachedAudioSources,
_cachedValueSources, set_cachedValueSources,
+ _sourcesLoading, set_sourcesLoading,
apiKey,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
@@ -129,7 +130,9 @@ async function loadCaptureTemplates() {
set_cachedCaptureTemplates(data.templates || []);
renderPictureSourcesList(_cachedStreams);
} catch (error) {
+ if (error.isAuth) return;
console.error('Error loading capture templates:', error);
+ showToast(t('streams.error.load'), 'error');
}
}
@@ -511,6 +514,8 @@ export async function deleteTemplate(templateId) {
// ===== Picture Sources =====
export async function loadPictureSources() {
+ if (_sourcesLoading) return;
+ set_sourcesLoading(true);
try {
const [filtersResp, ppResp, captResp, streamsResp, audioResp, valueResp] = await Promise.all([
_availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
@@ -546,10 +551,13 @@ export async function loadPictureSources() {
set_cachedStreams(data.streams || []);
renderPictureSourcesList(_cachedStreams);
} catch (error) {
+ if (error.isAuth) return;
console.error('Error loading picture sources:', error);
document.getElementById('streams-list').innerHTML = `
${t('streams.error.load')}: ${error.message}
`;
+ } finally {
+ set_sourcesLoading(false);
}
}
diff --git a/server/src/wled_controller/static/js/features/tabs.js b/server/src/wled_controller/static/js/features/tabs.js
index 73d39b1..6fa1af3 100644
--- a/server/src/wled_controller/static/js/features/tabs.js
+++ b/server/src/wled_controller/static/js/features/tabs.js
@@ -44,6 +44,11 @@ export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
} else {
if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling();
if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer();
+ // Clean up WebSockets when leaving targets tab
+ if (name !== 'targets') {
+ if (typeof window.disconnectAllKCWebSockets === 'function') window.disconnectAllKCWebSockets();
+ if (typeof window.disconnectAllLedPreviewWS === 'function') window.disconnectAllLedPreviewWS();
+ }
if (!apiKey || skipLoad) return;
if (name === 'streams') {
if (typeof window.loadPictureSources === 'function') window.loadPictureSources();
@@ -100,16 +105,15 @@ export function startAutoRefresh() {
}
setRefreshInterval(setInterval(() => {
- if (apiKey) {
- const activeTab = localStorage.getItem('activeTab') || 'dashboard';
- if (activeTab === 'targets') {
- // Skip refresh while user interacts with a picker or slider
- const panel = document.getElementById('targets-panel-content');
- if (panel && panel.contains(document.activeElement) && document.activeElement.matches('input')) return;
- if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
- } else if (activeTab === 'dashboard') {
- if (typeof window.loadDashboard === 'function') window.loadDashboard();
- }
+ if (!apiKey || document.hidden) return;
+ const activeTab = localStorage.getItem('activeTab') || 'dashboard';
+ if (activeTab === 'targets') {
+ // Skip refresh while user interacts with a picker or slider
+ const panel = document.getElementById('targets-panel-content');
+ if (panel && panel.contains(document.activeElement) && document.activeElement.matches('input')) return;
+ if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
+ } else if (activeTab === 'dashboard') {
+ if (typeof window.loadDashboard === 'function') window.loadDashboard();
}
}, dashboardPollInterval));
}
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js
index dff9311..0e90cd6 100644
--- a/server/src/wled_controller/static/js/features/targets.js
+++ b/server/src/wled_controller/static/js/features/targets.js
@@ -1003,6 +1003,10 @@ function disconnectLedPreviewWS(targetId) {
if (panel) panel.style.display = 'none';
}
+export function disconnectAllLedPreviewWS() {
+ Object.keys(ledPreviewWebSockets).forEach(id => disconnectLedPreviewWS(id));
+}
+
export function toggleLedPreview(targetId) {
const panel = document.getElementById(`led-preview-panel-${targetId}`);
if (!panel) return;