Replace auto-start with startup automation, add card colors to dashboard
- Add `startup` automation condition type that activates on server boot, replacing the per-target `auto_start` flag - Remove `auto_start` field from targets, scene snapshots, and all API layers - Remove auto-start UI section and star buttons from dashboard and target cards - Remove `color` field from scene presets (backend, API, modal, frontend) - Add card color support to scene preset cards (color picker + border style) - Show localStorage-backed card colors on all dashboard cards (targets, automations, sync clocks, scene presets) - Fix card color picker updating wrong card when duplicate data attributes exist by using closest() from picker wrapper instead of global querySelector - Add sync clocks step to Sources tab tutorial - Bump SW cache v9 → v10 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,10 @@ import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling }
|
||||
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
||||
import {
|
||||
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
||||
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
|
||||
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_HELP, ICON_SCENE,
|
||||
} from '../core/icons.js';
|
||||
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
||||
import { cardColorStyle } from '../core/card-colors.js';
|
||||
|
||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||
const MAX_FPS_SAMPLES = 120;
|
||||
@@ -21,7 +22,6 @@ let _fpsHistory = {}; // { targetId: number[] } — fps_actual
|
||||
let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
|
||||
let _fpsCharts = {}; // { targetId: Chart }
|
||||
let _lastRunningIds = []; // sorted target IDs from previous render
|
||||
let _lastAutoStartIds = ''; // comma-joined sorted auto-start IDs
|
||||
let _lastSyncClockIds = ''; // comma-joined sorted sync clock IDs
|
||||
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
||||
let _uptimeTimer = null;
|
||||
@@ -308,7 +308,8 @@ function renderDashboardSyncClock(clock) {
|
||||
clock.description ? escapeHtml(clock.description) : '',
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}">
|
||||
const scStyle = cardColorStyle(clock.id);
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}"${scStyle ? ` style="${scStyle}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_CLOCK}</span>
|
||||
<div>
|
||||
@@ -447,8 +448,6 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
// Build dynamic HTML (targets, automations)
|
||||
let dynamicHtml = '';
|
||||
let runningIds = [];
|
||||
let newAutoStartIds = '';
|
||||
|
||||
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
|
||||
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
||||
} else {
|
||||
@@ -465,10 +464,9 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
// Check if we can do an in-place metrics update (same targets, not first load)
|
||||
const newRunningIds = running.map(t => t.id).sort().join(',');
|
||||
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
||||
newAutoStartIds = enriched.filter(t => t.auto_start).map(t => t.id).sort().join(',');
|
||||
const newSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
||||
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
||||
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newAutoStartIds === _lastAutoStartIds && newSyncClockIds === _lastSyncClockIds;
|
||||
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newSyncClockIds === _lastSyncClockIds;
|
||||
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
||||
_updateRunningMetrics(running);
|
||||
_updateSyncClocksInPlace(syncClocks);
|
||||
@@ -489,52 +487,6 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoStartTargets = enriched.filter(t => t.auto_start);
|
||||
if (autoStartTargets.length > 0) {
|
||||
const autoStartCards = autoStartTargets.map(target => {
|
||||
const isRunning = !!(target.state && target.state.processing);
|
||||
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
|
||||
? `<span class="dashboard-badge-active">${t('automations.status.active')}</span>`
|
||||
: `<span class="dashboard-badge-stopped">${t('automations.status.inactive')}</span>`;
|
||||
const subtitle = subtitleParts.length ? `<div class="dashboard-target-subtitle">${escapeHtml(subtitleParts.join(' · '))}</div>` : '';
|
||||
const asNavSub = isLed ? 'led' : 'key_colors';
|
||||
const asNavSec = isLed ? 'led-targets' : 'kc-targets';
|
||||
const asNavAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-target-id="${target.id}" onclick="if(!event.target.closest('button')){navigateToCard('targets','${asNavSub}','${asNavSec}','${asNavAttr}','${target.id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
|
||||
<div>
|
||||
<div class="dashboard-target-name">${escapeHtml(target.name)} ${statusBadge}</div>
|
||||
${subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-action-btn ${isRunning ? 'stop' : 'start'}" onclick="${isRunning ? `dashboardStopTarget('${target.id}')` : `dashboardStartTarget('${target.id}')`}" title="${isRunning ? t('device.stop') : t('device.start')}">
|
||||
${isRunning ? ICON_STOP_PLAIN : ICON_START}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
const autoStartItems = `<div class="dashboard-autostart-grid">${autoStartCards}</div>`;
|
||||
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('autostart', t('autostart.title'), autoStartTargets.length)}
|
||||
${_sectionContent('autostart', autoStartItems)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (automations.length > 0) {
|
||||
const activeAutomations = automations.filter(a => a.is_active);
|
||||
const inactiveAutomations = automations.filter(a => !a.is_active);
|
||||
@@ -617,7 +569,6 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
}
|
||||
}
|
||||
_lastRunningIds = runningIds;
|
||||
_lastAutoStartIds = newAutoStartIds;
|
||||
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
||||
_cacheUptimeElements();
|
||||
await _initFpsCharts(runningIds);
|
||||
@@ -683,7 +634,8 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
healthDot = `<span class="health-dot ${cls}"></span>`;
|
||||
}
|
||||
|
||||
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}">
|
||||
const cStyle = cardColorStyle(target.id);
|
||||
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}"${cStyle ? ` style="${cStyle}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${icon}</span>
|
||||
<div>
|
||||
@@ -708,12 +660,12 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-autostart-btn${target.auto_start ? ' active' : ''}" onclick="dashboardToggleAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>
|
||||
<button class="dashboard-action-btn stop" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">${ICON_STOP_PLAIN}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}">
|
||||
const cStyle2 = cardColorStyle(target.id);
|
||||
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}"${cStyle2 ? ` style="${cStyle2}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${icon}</span>
|
||||
<div>
|
||||
@@ -723,7 +675,6 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
</div>
|
||||
<div class="dashboard-target-metrics"></div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-autostart-btn${target.auto_start ? ' active' : ''}" onclick="dashboardToggleAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>
|
||||
<button class="dashboard-action-btn start" onclick="dashboardStartTarget('${target.id}')" title="${t('device.button.start')}">${ICON_START}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -742,6 +693,7 @@ function renderDashboardAutomation(automation, sceneMap = new Map()) {
|
||||
const matchLabel = c.match_type === 'topmost' ? t('automations.condition.application.match_type.topmost') : t('automations.condition.application.match_type.running');
|
||||
return `${apps} (${matchLabel})`;
|
||||
}
|
||||
if (c.condition_type === 'startup') return t('automations.condition.startup');
|
||||
return c.condition_type;
|
||||
});
|
||||
const logic = automation.condition_logic === 'and' ? ' & ' : ' | ';
|
||||
@@ -758,7 +710,8 @@ function renderDashboardAutomation(automation, sceneMap = new Map()) {
|
||||
const scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null;
|
||||
const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected');
|
||||
|
||||
return `<div class="dashboard-target dashboard-automation dashboard-card-link" data-automation-id="${automation.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'automations','data-automation-id','${automation.id}')}">
|
||||
const aStyle = cardColorStyle(automation.id);
|
||||
return `<div class="dashboard-target dashboard-automation dashboard-card-link" data-automation-id="${automation.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'automations','data-automation-id','${automation.id}')}"${aStyle ? ` style="${aStyle}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_AUTOMATION}</span>
|
||||
<div>
|
||||
@@ -827,25 +780,6 @@ export async function dashboardStopTarget(targetId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardToggleAutoStart(targetId, enable) {
|
||||
try {
|
||||
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ auto_start: enable }),
|
||||
});
|
||||
if (response.ok) {
|
||||
showToast(t(enable ? 'autostart.toggle.enabled' : 'autostart.toggle.disabled'), 'success');
|
||||
loadDashboard();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.detail || t('dashboard.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(t('dashboard.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function dashboardStopAll() {
|
||||
try {
|
||||
const [targetsResp, statesResp] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user