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 {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
</div>
|
||||
<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>`;
|
||||
} else {
|
||||
@@ -555,8 +555,8 @@ function renderDashboardProfile(profile) {
|
||||
</div>
|
||||
</div>
|
||||
<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')}">
|
||||
${profile.enabled ? '⏸' : '▶'}
|
||||
<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 ? '⏹' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -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 = '<div class="devices-grid">';
|
||||
for (const p of profiles) {
|
||||
html += createProfileCard(p);
|
||||
html += createProfileCard(p, runningTargetIds);
|
||||
}
|
||||
html += `<div class="template-card add-template-card" onclick="openProfileEditor()">
|
||||
<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 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="card-actions">
|
||||
<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 ? '⏸' : '▶'}
|
||||
</button>
|
||||
</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) {
|
||||
try {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}с",
|
||||
|
||||
Reference in New Issue
Block a user