From 0a000cc44c7a0529d8d55311c0d5ff544b4a388c Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Fri, 20 Feb 2026 22:35:24 +0300 Subject: [PATCH] Fix Toggle All button state, stop icons, and Disable tooltip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Toggle All button always showing Start: /picture-targets list endpoint does not include processing state; now fetches /picture-targets/{id}/state per-target in parallel in both loadProfiles() and toggleProfileTargets() - Replace pause icons (⏸) with stop icons (⏹) in dashboard - Change profile automation toggle tooltip from 'Disabled' (status) to 'Disable' (action); add profiles.action.disable i18n key Co-Authored-By: Claude Sonnet 4.6 --- server/src/wled_controller/static/js/app.js | 3 +- .../static/js/features/dashboard.js | 8 +-- .../static/js/features/profiles.js | 67 ++++++++++++++++--- .../wled_controller/static/locales/en.json | 3 + .../wled_controller/static/locales/ru.json | 3 + 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 54d38f2..76a8db2 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -72,7 +72,7 @@ import { import { loadProfiles, openProfileEditor, closeProfileEditorModal, saveProfileEditor, addProfileCondition, - toggleProfileEnabled, deleteProfile, + toggleProfileEnabled, toggleProfileTargets, deleteProfile, } from './features/profiles.js'; // Layer 5: device-discovery, targets @@ -244,6 +244,7 @@ Object.assign(window, { saveProfileEditor, addProfileCondition, toggleProfileEnabled, + toggleProfileTargets, deleteProfile, // device-discovery diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index 64b397f..7ffcb1b 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -214,7 +214,7 @@ function _updateProfilesInPlace(profiles) { if (btn) { btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`; btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`); - btn.textContent = p.enabled ? '⏸' : '▶'; + btn.textContent = p.enabled ? '⏹' : '▶'; } } } @@ -491,7 +491,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
- +
`; } else { @@ -555,8 +555,8 @@ function renderDashboardProfile(profile) {
-
`; diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/profiles.js index 6dd2fc0..546d7bc 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/profiles.js @@ -25,11 +25,27 @@ export async function loadProfiles() { if (!container) return; try { - const resp = await fetchWithAuth('/profiles'); - if (!resp.ok) throw new Error('Failed to load profiles'); - const data = await resp.json(); + const [profilesResp, targetsResp] = await Promise.all([ + fetchWithAuth('/profiles'), + fetchWithAuth('/picture-targets'), + ]); + if (!profilesResp.ok) throw new Error('Failed to load profiles'); + const data = await profilesResp.json(); + const targetsData = targetsResp.ok ? await targetsResp.json() : { targets: [] }; + const allTargets = targetsData.targets || []; + // State is not included in the list response — fetch per-target in parallel + const stateResults = await Promise.all( + allTargets.map(tgt => + fetchWithAuth(`/picture-targets/${tgt.id}/state`) + .then(r => r.ok ? r.json() : null) + .catch(() => null) + ) + ); + const runningTargetIds = new Set( + allTargets.filter((_, i) => stateResults[i]?.processing).map(tgt => tgt.id) + ); set_profilesCache(data.profiles); - renderProfiles(data.profiles); + renderProfiles(data.profiles, runningTargetIds); } catch (error) { if (error.isAuth) return; console.error('Failed to load profiles:', error); @@ -37,12 +53,12 @@ export async function loadProfiles() { } } -function renderProfiles(profiles) { +function renderProfiles(profiles, runningTargetIds = new Set()) { const container = document.getElementById('profiles-content'); let html = '
'; for (const p of profiles) { - html += createProfileCard(p); + html += createProfileCard(p, runningTargetIds); } html += `
+
@@ -57,7 +73,7 @@ function renderProfiles(profiles) { }); } -function createProfileCard(profile) { +function createProfileCard(profile, runningTargetIds = new Set()) { const statusClass = !profile.enabled ? 'disabled' : profile.is_active ? 'active' : 'inactive'; const statusText = !profile.enabled ? t('profiles.status.disabled') : profile.is_active ? t('profiles.status.active') : t('profiles.status.inactive'); @@ -104,7 +120,15 @@ function createProfileCard(profile) {
${condPills}
- `; + })() : ''} +
@@ -361,6 +385,33 @@ export async function saveProfileEditor() { } } +export async function toggleProfileTargets(profileId) { + try { + const profileResp = await fetchWithAuth(`/profiles/${profileId}`); + if (!profileResp.ok) throw new Error('Failed to load profile'); + const profile = await profileResp.json(); + // Fetch actual processing state for each target in this profile + const stateResults = await Promise.all( + profile.target_ids.map(id => + fetchWithAuth(`/picture-targets/${id}/state`) + .then(r => r.ok ? r.json() : null) + .catch(() => null) + ) + ); + const runningSet = new Set( + profile.target_ids.filter((_, i) => stateResults[i]?.processing) + ); + const shouldStop = profile.target_ids.some(id => runningSet.has(id)); + await Promise.all(profile.target_ids.map(id => + fetchWithAuth(`/picture-targets/${id}/${shouldStop ? 'stop' : 'start'}`, { method: 'POST' }).catch(() => {}) + )); + loadProfiles(); + } catch (e) { + if (e.isAuth) return; + showToast(e.message, 'error'); + } +} + export async function toggleProfileEnabled(profileId, enable) { try { const action = enable ? 'enable' : 'disable'; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index e827824..3f95519 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -516,6 +516,7 @@ "profiles.status.active": "Active", "profiles.status.inactive": "Inactive", "profiles.status.disabled": "Disabled", + "profiles.action.disable": "Disable", "profiles.last_activated": "Last activated", "profiles.logic.and": " AND ", "profiles.logic.or": " OR ", @@ -525,6 +526,8 @@ "profiles.created": "Profile created", "profiles.deleted": "Profile deleted", "profiles.error.name_required": "Name is required", + "profiles.toggle_all.start": "Start all targets", + "profiles.toggle_all.stop": "Stop all targets", "time.hours_minutes": "{h}h {m}m", "time.minutes_seconds": "{m}m {s}s", "time.seconds": "{s}s", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index a5d2693..bd9cd4a 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -516,6 +516,7 @@ "profiles.status.active": "Активен", "profiles.status.inactive": "Неактивен", "profiles.status.disabled": "Отключён", + "profiles.action.disable": "Отключить", "profiles.last_activated": "Последняя активация", "profiles.logic.and": " И ", "profiles.logic.or": " ИЛИ ", @@ -525,6 +526,8 @@ "profiles.created": "Профиль создан", "profiles.deleted": "Профиль удалён", "profiles.error.name_required": "Введите название", + "profiles.toggle_all.start": "Запустить все цели", + "profiles.toggle_all.stop": "Остановить все цели", "time.hours_minutes": "{h}ч {m}м", "time.minutes_seconds": "{m}м {s}с", "time.seconds": "{s}с",