Fix Toggle All button state, stop icons, and Disable tooltip
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
loadProfiles, openProfileEditor, closeProfileEditorModal,
|
loadProfiles, openProfileEditor, closeProfileEditorModal,
|
||||||
saveProfileEditor, addProfileCondition,
|
saveProfileEditor, addProfileCondition,
|
||||||
toggleProfileEnabled, deleteProfile,
|
toggleProfileEnabled, toggleProfileTargets, deleteProfile,
|
||||||
} from './features/profiles.js';
|
} from './features/profiles.js';
|
||||||
|
|
||||||
// Layer 5: device-discovery, targets
|
// Layer 5: device-discovery, targets
|
||||||
@@ -244,6 +244,7 @@ Object.assign(window, {
|
|||||||
saveProfileEditor,
|
saveProfileEditor,
|
||||||
addProfileCondition,
|
addProfileCondition,
|
||||||
toggleProfileEnabled,
|
toggleProfileEnabled,
|
||||||
|
toggleProfileTargets,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
|
|
||||||
// device-discovery
|
// device-discovery
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ function _updateProfilesInPlace(profiles) {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`;
|
btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`;
|
||||||
btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`);
|
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
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
<button class="btn btn-icon btn-warning" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">⏸</button>
|
<button class="btn btn-icon btn-warning" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">⏹</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
@@ -555,8 +555,8 @@ function renderDashboardProfile(profile) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.status.disabled') : t('profiles.status.active')}">
|
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||||
${profile.enabled ? '⏸' : '▶'}
|
${profile.enabled ? '⏹' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -25,11 +25,27 @@ export async function loadProfiles() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetchWithAuth('/profiles');
|
const [profilesResp, targetsResp] = await Promise.all([
|
||||||
if (!resp.ok) throw new Error('Failed to load profiles');
|
fetchWithAuth('/profiles'),
|
||||||
const data = await resp.json();
|
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);
|
set_profilesCache(data.profiles);
|
||||||
renderProfiles(data.profiles);
|
renderProfiles(data.profiles, runningTargetIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.isAuth) return;
|
if (error.isAuth) return;
|
||||||
console.error('Failed to load profiles:', error);
|
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');
|
const container = document.getElementById('profiles-content');
|
||||||
|
|
||||||
let html = '<div class="devices-grid">';
|
let html = '<div class="devices-grid">';
|
||||||
for (const p of profiles) {
|
for (const p of profiles) {
|
||||||
html += createProfileCard(p);
|
html += createProfileCard(p, runningTargetIds);
|
||||||
}
|
}
|
||||||
html += `<div class="template-card add-template-card" onclick="openProfileEditor()">
|
html += `<div class="template-card add-template-card" onclick="openProfileEditor()">
|
||||||
<div class="add-template-icon">+</div>
|
<div class="add-template-icon">+</div>
|
||||||
@@ -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 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');
|
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) {
|
|||||||
<div class="stream-card-props">${condPills}</div>
|
<div class="stream-card-props">${condPills}</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">⚙️</button>
|
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">⚙️</button>
|
||||||
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.status.disabled') : t('profiles.status.active')}">
|
${profile.target_ids.length > 0 ? (() => {
|
||||||
|
const anyRunning = profile.target_ids.some(id => runningTargetIds.has(id));
|
||||||
|
return `<button class="btn btn-icon ${anyRunning ? 'btn-warning' : 'btn-success'}"
|
||||||
|
onclick="toggleProfileTargets('${profile.id}')"
|
||||||
|
title="${anyRunning ? t('profiles.toggle_all.stop') : t('profiles.toggle_all.start')}">
|
||||||
|
${anyRunning ? '⏹' : '▶️'}
|
||||||
|
</button>`;
|
||||||
|
})() : ''}
|
||||||
|
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||||
${profile.enabled ? '⏸' : '▶'}
|
${profile.enabled ? '⏸' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -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) {
|
export async function toggleProfileEnabled(profileId, enable) {
|
||||||
try {
|
try {
|
||||||
const action = enable ? 'enable' : 'disable';
|
const action = enable ? 'enable' : 'disable';
|
||||||
|
|||||||
@@ -516,6 +516,7 @@
|
|||||||
"profiles.status.active": "Active",
|
"profiles.status.active": "Active",
|
||||||
"profiles.status.inactive": "Inactive",
|
"profiles.status.inactive": "Inactive",
|
||||||
"profiles.status.disabled": "Disabled",
|
"profiles.status.disabled": "Disabled",
|
||||||
|
"profiles.action.disable": "Disable",
|
||||||
"profiles.last_activated": "Last activated",
|
"profiles.last_activated": "Last activated",
|
||||||
"profiles.logic.and": " AND ",
|
"profiles.logic.and": " AND ",
|
||||||
"profiles.logic.or": " OR ",
|
"profiles.logic.or": " OR ",
|
||||||
@@ -525,6 +526,8 @@
|
|||||||
"profiles.created": "Profile created",
|
"profiles.created": "Profile created",
|
||||||
"profiles.deleted": "Profile deleted",
|
"profiles.deleted": "Profile deleted",
|
||||||
"profiles.error.name_required": "Name is required",
|
"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.hours_minutes": "{h}h {m}m",
|
||||||
"time.minutes_seconds": "{m}m {s}s",
|
"time.minutes_seconds": "{m}m {s}s",
|
||||||
"time.seconds": "{s}s",
|
"time.seconds": "{s}s",
|
||||||
|
|||||||
@@ -516,6 +516,7 @@
|
|||||||
"profiles.status.active": "Активен",
|
"profiles.status.active": "Активен",
|
||||||
"profiles.status.inactive": "Неактивен",
|
"profiles.status.inactive": "Неактивен",
|
||||||
"profiles.status.disabled": "Отключён",
|
"profiles.status.disabled": "Отключён",
|
||||||
|
"profiles.action.disable": "Отключить",
|
||||||
"profiles.last_activated": "Последняя активация",
|
"profiles.last_activated": "Последняя активация",
|
||||||
"profiles.logic.and": " И ",
|
"profiles.logic.and": " И ",
|
||||||
"profiles.logic.or": " ИЛИ ",
|
"profiles.logic.or": " ИЛИ ",
|
||||||
@@ -525,6 +526,8 @@
|
|||||||
"profiles.created": "Профиль создан",
|
"profiles.created": "Профиль создан",
|
||||||
"profiles.deleted": "Профиль удалён",
|
"profiles.deleted": "Профиль удалён",
|
||||||
"profiles.error.name_required": "Введите название",
|
"profiles.error.name_required": "Введите название",
|
||||||
|
"profiles.toggle_all.start": "Запустить все цели",
|
||||||
|
"profiles.toggle_all.stop": "Остановить все цели",
|
||||||
"time.hours_minutes": "{h}ч {m}м",
|
"time.hours_minutes": "{h}ч {m}м",
|
||||||
"time.minutes_seconds": "{m}м {s}с",
|
"time.minutes_seconds": "{m}м {s}с",
|
||||||
"time.seconds": "{s}с",
|
"time.seconds": "{s}с",
|
||||||
|
|||||||
Reference in New Issue
Block a user