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:
@@ -24,6 +24,7 @@ from wled_controller.storage.automation import (
|
||||
Condition,
|
||||
DisplayStateCondition,
|
||||
MQTTCondition,
|
||||
StartupCondition,
|
||||
SystemIdleCondition,
|
||||
TimeOfDayCondition,
|
||||
WebhookCondition,
|
||||
@@ -70,6 +71,8 @@ def _condition_from_schema(s: ConditionSchema) -> Condition:
|
||||
return WebhookCondition(
|
||||
token=s.token or secrets.token_hex(16),
|
||||
)
|
||||
if s.condition_type == "startup":
|
||||
return StartupCondition()
|
||||
raise ValueError(f"Unknown condition type: {s.condition_type}")
|
||||
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
||||
adaptive_fps=target.adaptive_fps,
|
||||
protocol=target.protocol,
|
||||
description=target.description,
|
||||
auto_start=target.auto_start,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -117,7 +117,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
||||
picture_source_id=target.picture_source_id,
|
||||
key_colors_settings=_kc_settings_to_schema(target.settings),
|
||||
description=target.description,
|
||||
auto_start=target.auto_start,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -127,7 +127,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
||||
name=target.name,
|
||||
target_type=target.target_type,
|
||||
description=target.description,
|
||||
auto_start=target.auto_start,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -169,7 +169,6 @@ async def create_target(
|
||||
picture_source_id=data.picture_source_id,
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
auto_start=data.auto_start,
|
||||
)
|
||||
|
||||
# Register in processor manager
|
||||
@@ -288,7 +287,6 @@ async def update_target(
|
||||
protocol=data.protocol,
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
auto_start=data.auto_start,
|
||||
)
|
||||
|
||||
# Detect KC brightness VS change (inside key_colors_settings)
|
||||
|
||||
@@ -37,14 +37,12 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
||||
id=preset.id,
|
||||
name=preset.name,
|
||||
description=preset.description,
|
||||
color=preset.color,
|
||||
targets=[{
|
||||
"target_id": t.target_id,
|
||||
"running": t.running,
|
||||
"color_strip_source_id": t.color_strip_source_id,
|
||||
"brightness_value_source_id": t.brightness_value_source_id,
|
||||
"fps": t.fps,
|
||||
"auto_start": t.auto_start,
|
||||
} for t in preset.targets],
|
||||
order=preset.order,
|
||||
created_at=preset.created_at,
|
||||
@@ -76,7 +74,6 @@ async def create_scene_preset(
|
||||
id=f"scene_{uuid.uuid4().hex[:8]}",
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
color=data.color,
|
||||
targets=targets,
|
||||
order=store.count(),
|
||||
created_at=now,
|
||||
@@ -143,7 +140,6 @@ async def update_scene_preset(
|
||||
preset_id,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
color=data.color,
|
||||
order=data.order,
|
||||
)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -65,7 +65,6 @@ class PictureTargetCreate(BaseModel):
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
auto_start: bool = Field(default=False, description="Auto-start on server boot")
|
||||
|
||||
|
||||
class PictureTargetUpdate(BaseModel):
|
||||
@@ -86,7 +85,6 @@ class PictureTargetUpdate(BaseModel):
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
auto_start: Optional[bool] = Field(None, description="Auto-start on server boot")
|
||||
|
||||
|
||||
class PictureTargetResponse(BaseModel):
|
||||
@@ -109,7 +107,6 @@ class PictureTargetResponse(BaseModel):
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
auto_start: bool = Field(default=False, description="Auto-start on server boot")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class TargetSnapshotSchema(BaseModel):
|
||||
color_strip_source_id: str = ""
|
||||
brightness_value_source_id: str = ""
|
||||
fps: int = 30
|
||||
auto_start: bool = False
|
||||
|
||||
|
||||
class ScenePresetCreate(BaseModel):
|
||||
@@ -20,7 +19,6 @@ class ScenePresetCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||
description: str = Field(default="", max_length=500)
|
||||
color: str = Field(default="#4fc3f7", description="Card accent color")
|
||||
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
|
||||
|
||||
|
||||
@@ -29,7 +27,6 @@ class ScenePresetUpdate(BaseModel):
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
color: Optional[str] = None
|
||||
order: Optional[int] = None
|
||||
|
||||
|
||||
@@ -39,7 +36,6 @@ class ScenePresetResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
color: str
|
||||
targets: List[TargetSnapshotSchema]
|
||||
order: int
|
||||
created_at: datetime
|
||||
|
||||
@@ -13,6 +13,7 @@ from wled_controller.storage.automation import (
|
||||
Condition,
|
||||
DisplayStateCondition,
|
||||
MQTTCondition,
|
||||
StartupCondition,
|
||||
SystemIdleCondition,
|
||||
TimeOfDayCondition,
|
||||
WebhookCondition,
|
||||
@@ -204,7 +205,7 @@ class AutomationEngine:
|
||||
fullscreen_procs: Set[str],
|
||||
idle_seconds: Optional[float], display_state: Optional[str],
|
||||
) -> bool:
|
||||
if isinstance(condition, AlwaysCondition):
|
||||
if isinstance(condition, (AlwaysCondition, StartupCondition)):
|
||||
return True
|
||||
if isinstance(condition, ApplicationCondition):
|
||||
return self._evaluate_app_condition(condition, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs)
|
||||
|
||||
@@ -38,7 +38,6 @@ def capture_current_snapshot(
|
||||
color_strip_source_id=getattr(t, "color_strip_source_id", ""),
|
||||
brightness_value_source_id=getattr(t, "brightness_value_source_id", ""),
|
||||
fps=getattr(t, "fps", 30),
|
||||
auto_start=getattr(t, "auto_start", False),
|
||||
))
|
||||
|
||||
return targets
|
||||
@@ -83,9 +82,6 @@ async def apply_scene_state(
|
||||
changed["brightness_value_source_id"] = ts.brightness_value_source_id
|
||||
if getattr(target, "fps", None) != ts.fps:
|
||||
changed["fps"] = ts.fps
|
||||
if getattr(target, "auto_start", None) != ts.auto_start:
|
||||
changed["auto_start"] = ts.auto_start
|
||||
|
||||
if changed:
|
||||
target.update_fields(**changed)
|
||||
target_store.update_target(ts.target_id, **changed)
|
||||
|
||||
@@ -188,19 +188,6 @@ async def lifespan(app: FastAPI):
|
||||
# Start auto-backup engine (periodic configuration backups)
|
||||
await auto_backup_engine.start()
|
||||
|
||||
# Auto-start targets with auto_start=True
|
||||
auto_started = 0
|
||||
for target in targets:
|
||||
if getattr(target, "auto_start", False):
|
||||
try:
|
||||
await processor_manager.start_processing(target.id)
|
||||
auto_started += 1
|
||||
logger.info(f"Auto-started target: {target.name} ({target.id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to auto-start target {target.id}: {e}")
|
||||
if auto_started:
|
||||
logger.info(f"Auto-started {auto_started} target(s)")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from './features/devices.js';
|
||||
import {
|
||||
loadDashboard, stopUptimeTimer,
|
||||
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
|
||||
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
|
||||
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
|
||||
toggleDashboardSection, changeDashboardPollInterval,
|
||||
} from './features/dashboard.js';
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
startTargetProcessing, stopTargetProcessing,
|
||||
stopAllLedTargets, stopAllKCTargets,
|
||||
startTargetOverlay, stopTargetOverlay, deleteTarget,
|
||||
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
|
||||
cloneTarget, toggleLedPreview,
|
||||
expandAllTargetSections, collapseAllTargetSections,
|
||||
disconnectAllLedPreviewWS,
|
||||
} from './features/targets.js';
|
||||
@@ -211,7 +211,6 @@ Object.assign(window, {
|
||||
dashboardToggleAutomation,
|
||||
dashboardStartTarget,
|
||||
dashboardStopTarget,
|
||||
dashboardToggleAutoStart,
|
||||
dashboardStopAll,
|
||||
dashboardPauseClock,
|
||||
dashboardResumeClock,
|
||||
@@ -356,7 +355,6 @@ Object.assign(window, {
|
||||
deleteTarget,
|
||||
cloneTarget,
|
||||
toggleLedPreview,
|
||||
toggleTargetAutoStart,
|
||||
disconnectAllLedPreviewWS,
|
||||
|
||||
// color-strip sources
|
||||
|
||||
@@ -59,7 +59,10 @@ export function cardColorButton(entityId, cardAttr) {
|
||||
|
||||
registerColorPicker(pickerId, (hex) => {
|
||||
setCardColor(entityId, hex);
|
||||
const card = document.querySelector(`[${cardAttr}="${entityId}"]`);
|
||||
// Find the card that contains this picker (not a global querySelector
|
||||
// which could match a dashboard compact card first)
|
||||
const wrapper = document.getElementById(`cp-wrap-${pickerId}`);
|
||||
const card = wrapper?.closest(`[${cardAttr}]`);
|
||||
if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,9 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
||||
if (c.condition_type === 'always') {
|
||||
return `<span class="stream-card-prop">${ICON_OK} ${t('automations.condition.always')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'startup') {
|
||||
return `<span class="stream-card-prop">${ICON_START} ${t('automations.condition.startup')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'application') {
|
||||
const apps = (c.apps || []).join(', ');
|
||||
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
|
||||
@@ -384,6 +387,7 @@ function addAutomationConditionRow(condition) {
|
||||
<div class="condition-header">
|
||||
<select class="condition-type-select">
|
||||
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('automations.condition.always')}</option>
|
||||
<option value="startup" ${condType === 'startup' ? 'selected' : ''}>${t('automations.condition.startup')}</option>
|
||||
<option value="application" ${condType === 'application' ? 'selected' : ''}>${t('automations.condition.application')}</option>
|
||||
<option value="time_of_day" ${condType === 'time_of_day' ? 'selected' : ''}>${t('automations.condition.time_of_day')}</option>
|
||||
<option value="system_idle" ${condType === 'system_idle' ? 'selected' : ''}>${t('automations.condition.system_idle')}</option>
|
||||
@@ -404,6 +408,10 @@ function addAutomationConditionRow(condition) {
|
||||
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.always.hint')}</small>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'startup') {
|
||||
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.startup.hint')}</small>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'time_of_day') {
|
||||
const startTime = data.start_time || '00:00';
|
||||
const endTime = data.end_time || '23:59';
|
||||
@@ -612,6 +620,8 @@ function getAutomationEditorConditions() {
|
||||
const condType = typeSelect ? typeSelect.value : 'application';
|
||||
if (condType === 'always') {
|
||||
conditions.push({ condition_type: 'always' });
|
||||
} else if (condType === 'startup') {
|
||||
conditions.push({ condition_type: 'startup' });
|
||||
} else if (condType === 'time_of_day') {
|
||||
conditions.push({
|
||||
condition_type: 'time_of_day',
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -122,7 +122,6 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
||||
return wrapCard({
|
||||
dataAttr: 'data-kc-target-id',
|
||||
id: target.id,
|
||||
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>`,
|
||||
removeOnclick: `deleteKCTarget('${target.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||
} from '../core/icons.js';
|
||||
import { scenePresetsCache } from '../core/state.js';
|
||||
import { cardColorStyle, cardColorButton } from '../core/card-colors.js';
|
||||
|
||||
let _editingId = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
@@ -24,7 +25,6 @@ class ScenePresetEditorModal extends Modal {
|
||||
return {
|
||||
name: document.getElementById('scene-preset-editor-name').value,
|
||||
description: document.getElementById('scene-preset-editor-description').value,
|
||||
color: document.getElementById('scene-preset-editor-color').value,
|
||||
targets: items,
|
||||
};
|
||||
}
|
||||
@@ -40,7 +40,6 @@ export const csScenes = new CardSection('scenes', {
|
||||
|
||||
export function createSceneCard(preset) {
|
||||
const targetCount = (preset.targets || []).length;
|
||||
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
||||
|
||||
const meta = [
|
||||
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
@@ -48,7 +47,8 @@ export function createSceneCard(preset) {
|
||||
|
||||
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
|
||||
|
||||
return `<div class="card" data-scene-id="${preset.id}" style="${colorStyle}">
|
||||
const colorStyle = cardColorStyle(preset.id);
|
||||
return `<div class="card" data-scene-id="${preset.id}"${colorStyle ? ` style="${colorStyle}"` : ''}>
|
||||
<div class="card-top-actions">
|
||||
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">✕</button>
|
||||
</div>
|
||||
@@ -64,6 +64,7 @@ export function createSceneCard(preset) {
|
||||
<button class="btn btn-icon btn-secondary" onclick="editScenePreset('${preset.id}')" title="${t('scenes.edit')}">${ICON_EDIT}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="recaptureScenePreset('${preset.id}')" title="${t('scenes.recapture')}">${ICON_REFRESH}</button>
|
||||
<button class="btn btn-icon btn-success" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
${cardColorButton(preset.id, 'data-scene-id')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -84,14 +85,14 @@ export function renderScenePresetsSection(presets) {
|
||||
}
|
||||
|
||||
function _renderDashboardPresetCard(preset) {
|
||||
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
||||
const targetCount = (preset.targets || []).length;
|
||||
|
||||
const subtitle = [
|
||||
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
].filter(Boolean).join(' \u00b7 ');
|
||||
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
|
||||
const pStyle = cardColorStyle(preset.id);
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}"${pStyle ? ` style="${pStyle}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
||||
<div>
|
||||
@@ -113,7 +114,7 @@ export async function openScenePresetCapture() {
|
||||
document.getElementById('scene-preset-editor-id').value = '';
|
||||
document.getElementById('scene-preset-editor-name').value = '';
|
||||
document.getElementById('scene-preset-editor-description').value = '';
|
||||
document.getElementById('scene-preset-editor-color').value = '#4fc3f7';
|
||||
|
||||
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
@@ -149,7 +150,6 @@ export async function editScenePreset(presetId) {
|
||||
document.getElementById('scene-preset-editor-id').value = presetId;
|
||||
document.getElementById('scene-preset-editor-name').value = preset.name;
|
||||
document.getElementById('scene-preset-editor-description').value = preset.description || '';
|
||||
document.getElementById('scene-preset-editor-color').value = preset.color || '#4fc3f7';
|
||||
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||
|
||||
// Hide target selector in edit mode (metadata only)
|
||||
@@ -168,7 +168,6 @@ export async function editScenePreset(presetId) {
|
||||
export async function saveScenePreset() {
|
||||
const name = document.getElementById('scene-preset-editor-name').value.trim();
|
||||
const description = document.getElementById('scene-preset-editor-description').value.trim();
|
||||
const color = document.getElementById('scene-preset-editor-color').value;
|
||||
const errorEl = document.getElementById('scene-preset-editor-error');
|
||||
|
||||
if (!name) {
|
||||
@@ -182,14 +181,14 @@ export async function saveScenePreset() {
|
||||
if (_editingId) {
|
||||
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name, description, color }),
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
} else {
|
||||
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId);
|
||||
resp = await fetchWithAuth('/scene-presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, description, color, target_ids }),
|
||||
body: JSON.stringify({ name, description, target_ids }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -864,7 +864,6 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
||||
return wrapCard({
|
||||
dataAttr: 'data-target-id',
|
||||
id: target.id,
|
||||
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>`,
|
||||
removeOnclick: `deleteTarget('${target.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
@@ -1064,25 +1063,6 @@ export async function cloneTarget(targetId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleTargetAutoStart(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');
|
||||
loadTargetsTab();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.detail || t('target.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle auto-start:', error);
|
||||
showToast(t('target.error.autostart_toggle_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTarget(targetId) {
|
||||
const confirmed = await showConfirm(t('targets.delete.confirm'));
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -56,7 +56,8 @@ const sourcesTourSteps = [
|
||||
{ selector: '[data-stream-tab="static_image"]', textKey: 'tour.src.static', position: 'bottom' },
|
||||
{ selector: '[data-stream-tab="processed"]', textKey: 'tour.src.processed', position: 'bottom' },
|
||||
{ selector: '[data-stream-tab="audio"]', textKey: 'tour.src.audio', position: 'bottom' },
|
||||
{ selector: '[data-stream-tab="value"]', textKey: 'tour.src.value', position: 'bottom' }
|
||||
{ selector: '[data-stream-tab="value"]', textKey: 'tour.src.value', position: 'bottom' },
|
||||
{ selector: '[data-stream-tab="sync"]', textKey: 'tour.src.sync', position: 'bottom' }
|
||||
];
|
||||
|
||||
const automationsTutorialSteps = [
|
||||
|
||||
@@ -262,6 +262,7 @@
|
||||
"tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.",
|
||||
"tour.src.audio": "Audio — analyze microphone or system audio for reactive LED effects.",
|
||||
"tour.src.value": "Value — numeric data sources used as conditions in automations.",
|
||||
"tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.",
|
||||
"tour.auto.list": "Automations — automate scene activation based on time, audio, or value conditions.",
|
||||
"tour.auto.add": "Click + to create a new automation with conditions and a scene to activate.",
|
||||
"tour.auto.card": "Each card shows automation status, conditions, and quick controls to edit or toggle.",
|
||||
@@ -584,7 +585,9 @@
|
||||
"automations.conditions.add": "Add Condition",
|
||||
"automations.conditions.empty": "No conditions — automation is always active when enabled",
|
||||
"automations.condition.always": "Always",
|
||||
"automations.condition.always.hint": "Automation activates immediately when enabled and stays active. Use this to auto-start scenes on server startup.",
|
||||
"automations.condition.always.hint": "Automation activates immediately when enabled and stays active.",
|
||||
"automations.condition.startup": "Startup",
|
||||
"automations.condition.startup.hint": "Activates when the server starts and stays active while enabled.",
|
||||
"automations.condition.application": "Application",
|
||||
"automations.condition.application.apps": "Applications:",
|
||||
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
||||
@@ -657,8 +660,6 @@
|
||||
"scenes.name.placeholder": "My Scene",
|
||||
"scenes.description": "Description:",
|
||||
"scenes.description.hint": "Optional description of what this scene does",
|
||||
"scenes.color": "Card Color:",
|
||||
"scenes.color.hint": "Accent color for the scene card on the dashboard",
|
||||
"scenes.targets": "Targets:",
|
||||
"scenes.targets.hint": "Select which targets to include in this scene snapshot",
|
||||
"scenes.capture": "Capture",
|
||||
@@ -680,10 +681,6 @@
|
||||
"scenes.error.activate_failed": "Failed to activate scene",
|
||||
"scenes.error.recapture_failed": "Failed to recapture scene",
|
||||
"scenes.error.delete_failed": "Failed to delete scene",
|
||||
"autostart.title": "Auto-start Targets",
|
||||
"autostart.toggle.enabled": "Auto-start enabled",
|
||||
"autostart.toggle.disabled": "Auto-start disabled",
|
||||
"autostart.goto_target": "Go to target",
|
||||
"time.hours_minutes": "{h}h {m}m",
|
||||
"time.minutes_seconds": "{m}m {s}s",
|
||||
"time.seconds": "{s}s",
|
||||
@@ -1110,13 +1107,11 @@
|
||||
"dashboard.error.automation_toggle_failed": "Failed to toggle automation",
|
||||
"dashboard.error.start_failed": "Failed to start processing",
|
||||
"dashboard.error.stop_failed": "Failed to stop processing",
|
||||
"dashboard.error.autostart_toggle_failed": "Failed to toggle auto-start",
|
||||
"dashboard.error.stop_all": "Failed to stop all targets",
|
||||
"target.error.editor_open_failed": "Failed to open target editor",
|
||||
"target.error.start_failed": "Failed to start target",
|
||||
"target.error.stop_failed": "Failed to stop target",
|
||||
"target.error.clone_failed": "Failed to clone target",
|
||||
"target.error.autostart_toggle_failed": "Failed to toggle auto-start",
|
||||
"target.error.delete_failed": "Failed to delete target",
|
||||
"targets.stop_all.none_running": "No targets are currently running",
|
||||
"targets.stop_all.stopped": "Stopped {count} target(s)",
|
||||
|
||||
@@ -262,6 +262,7 @@
|
||||
"tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.",
|
||||
"tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.",
|
||||
"tour.src.value": "Значения — числовые источники данных для условий автоматизаций.",
|
||||
"tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.",
|
||||
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
|
||||
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
|
||||
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
|
||||
@@ -584,7 +585,9 @@
|
||||
"automations.conditions.add": "Добавить условие",
|
||||
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена",
|
||||
"automations.condition.always": "Всегда",
|
||||
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной. Используйте для автозапуска сцен при старте сервера.",
|
||||
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.",
|
||||
"automations.condition.startup": "Автозапуск",
|
||||
"automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.",
|
||||
"automations.condition.application": "Приложение",
|
||||
"automations.condition.application.apps": "Приложения:",
|
||||
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
|
||||
@@ -657,8 +660,6 @@
|
||||
"scenes.name.placeholder": "Моя сцена",
|
||||
"scenes.description": "Описание:",
|
||||
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
||||
"scenes.color": "Цвет карточки:",
|
||||
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
|
||||
"scenes.targets": "Цели:",
|
||||
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
|
||||
"scenes.capture": "Захват",
|
||||
@@ -680,10 +681,6 @@
|
||||
"scenes.error.activate_failed": "Не удалось активировать сцену",
|
||||
"scenes.error.recapture_failed": "Не удалось перезахватить сцену",
|
||||
"scenes.error.delete_failed": "Не удалось удалить сцену",
|
||||
"autostart.title": "Автозапуск целей",
|
||||
"autostart.toggle.enabled": "Автозапуск включён",
|
||||
"autostart.toggle.disabled": "Автозапуск отключён",
|
||||
"autostart.goto_target": "Перейти к цели",
|
||||
"time.hours_minutes": "{h}ч {m}м",
|
||||
"time.minutes_seconds": "{m}м {s}с",
|
||||
"time.seconds": "{s}с",
|
||||
@@ -1110,13 +1107,11 @@
|
||||
"dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию",
|
||||
"dashboard.error.start_failed": "Не удалось запустить обработку",
|
||||
"dashboard.error.stop_failed": "Не удалось остановить обработку",
|
||||
"dashboard.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
|
||||
"dashboard.error.stop_all": "Не удалось остановить все цели",
|
||||
"target.error.editor_open_failed": "Не удалось открыть редактор цели",
|
||||
"target.error.start_failed": "Не удалось запустить цель",
|
||||
"target.error.stop_failed": "Не удалось остановить цель",
|
||||
"target.error.clone_failed": "Не удалось клонировать цель",
|
||||
"target.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
|
||||
"target.error.delete_failed": "Не удалось удалить цель",
|
||||
"targets.stop_all.none_running": "Нет запущенных целей",
|
||||
"targets.stop_all.stopped": "Остановлено целей: {count}",
|
||||
|
||||
@@ -262,6 +262,7 @@
|
||||
"tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。",
|
||||
"tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。",
|
||||
"tour.src.value": "数值 — 用于自动化条件的数字数据源。",
|
||||
"tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。",
|
||||
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
|
||||
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
|
||||
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
|
||||
@@ -584,7 +585,9 @@
|
||||
"automations.conditions.add": "添加条件",
|
||||
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态",
|
||||
"automations.condition.always": "始终",
|
||||
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。用于服务器启动时自动激活场景。",
|
||||
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。",
|
||||
"automations.condition.startup": "启动",
|
||||
"automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。",
|
||||
"automations.condition.application": "应用程序",
|
||||
"automations.condition.application.apps": "应用程序:",
|
||||
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)",
|
||||
@@ -657,8 +660,6 @@
|
||||
"scenes.name.placeholder": "我的场景",
|
||||
"scenes.description": "描述:",
|
||||
"scenes.description.hint": "此场景功能的可选描述",
|
||||
"scenes.color": "卡片颜色:",
|
||||
"scenes.color.hint": "仪表盘上场景卡片的强调色",
|
||||
"scenes.targets": "目标:",
|
||||
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
|
||||
"scenes.capture": "捕获",
|
||||
@@ -680,10 +681,6 @@
|
||||
"scenes.error.activate_failed": "激活场景失败",
|
||||
"scenes.error.recapture_failed": "重新捕获场景失败",
|
||||
"scenes.error.delete_failed": "删除场景失败",
|
||||
"autostart.title": "自动启动目标",
|
||||
"autostart.toggle.enabled": "自动启动已启用",
|
||||
"autostart.toggle.disabled": "自动启动已禁用",
|
||||
"autostart.goto_target": "跳转到目标",
|
||||
"time.hours_minutes": "{h}时 {m}分",
|
||||
"time.minutes_seconds": "{m}分 {s}秒",
|
||||
"time.seconds": "{s}秒",
|
||||
@@ -1110,13 +1107,11 @@
|
||||
"dashboard.error.automation_toggle_failed": "切换自动化失败",
|
||||
"dashboard.error.start_failed": "启动处理失败",
|
||||
"dashboard.error.stop_failed": "停止处理失败",
|
||||
"dashboard.error.autostart_toggle_failed": "切换自动启动失败",
|
||||
"dashboard.error.stop_all": "停止所有目标失败",
|
||||
"target.error.editor_open_failed": "打开目标编辑器失败",
|
||||
"target.error.start_failed": "启动目标失败",
|
||||
"target.error.stop_failed": "停止目标失败",
|
||||
"target.error.clone_failed": "克隆目标失败",
|
||||
"target.error.autostart_toggle_failed": "切换自动启动失败",
|
||||
"target.error.delete_failed": "删除目标失败",
|
||||
"targets.stop_all.none_running": "当前没有运行中的目标",
|
||||
"targets.stop_all.stopped": "已停止 {count} 个目标",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Navigation: network-first with offline fallback
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'ledgrab-v9';
|
||||
const CACHE_NAME = 'ledgrab-v10';
|
||||
|
||||
// Only pre-cache static assets (no auth required).
|
||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||
|
||||
@@ -32,6 +32,8 @@ class Condition:
|
||||
return MQTTCondition.from_dict(data)
|
||||
if ct == "webhook":
|
||||
return WebhookCondition.from_dict(data)
|
||||
if ct == "startup":
|
||||
return StartupCondition.from_dict(data)
|
||||
raise ValueError(f"Unknown condition type: {ct}")
|
||||
|
||||
|
||||
@@ -177,6 +179,17 @@ class WebhookCondition(Condition):
|
||||
return cls(token=data.get("token", ""))
|
||||
|
||||
|
||||
@dataclass
|
||||
class StartupCondition(Condition):
|
||||
"""Activate when the server starts — stays active while enabled."""
|
||||
|
||||
condition_type: str = "startup"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "StartupCondition":
|
||||
return cls()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Automation:
|
||||
"""Automation that activates a scene preset based on conditions."""
|
||||
|
||||
@@ -100,9 +100,9 @@ class KeyColorsPictureTarget(PictureTarget):
|
||||
|
||||
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
||||
settings=None, key_colors_settings=None, description=None,
|
||||
auto_start=None, **_kwargs) -> None:
|
||||
**_kwargs) -> None:
|
||||
"""Apply mutable field updates for KC targets."""
|
||||
super().update_fields(name=name, description=description, auto_start=auto_start)
|
||||
super().update_fields(name=name, description=description)
|
||||
if picture_source_id is not None:
|
||||
self.picture_source_id = picture_source_id
|
||||
if key_colors_settings is not None:
|
||||
@@ -130,7 +130,6 @@ class KeyColorsPictureTarget(PictureTarget):
|
||||
picture_source_id=data.get("picture_source_id", ""),
|
||||
settings=settings,
|
||||
description=data.get("description"),
|
||||
auto_start=data.get("auto_start", False),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ class PictureTarget:
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
auto_start: bool = False
|
||||
|
||||
def register_with_manager(self, manager) -> None:
|
||||
"""Register this target with the processor manager. Subclasses override."""
|
||||
@@ -27,14 +26,12 @@ class PictureTarget:
|
||||
|
||||
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
||||
settings=None, key_colors_settings=None, description=None,
|
||||
auto_start=None) -> None:
|
||||
**_kwargs) -> None:
|
||||
"""Apply mutable field updates. Base handles common fields; subclasses handle type-specific ones."""
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if description is not None:
|
||||
self.description = description
|
||||
if auto_start is not None:
|
||||
self.auto_start = auto_start
|
||||
|
||||
@property
|
||||
def has_picture_source(self) -> bool:
|
||||
@@ -48,7 +45,6 @@ class PictureTarget:
|
||||
"name": self.name,
|
||||
"target_type": self.target_type,
|
||||
"description": self.description,
|
||||
"auto_start": self.auto_start,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ class PictureTargetStore:
|
||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||
description: Optional[str] = None,
|
||||
picture_source_id: str = "",
|
||||
auto_start: bool = False,
|
||||
) -> PictureTarget:
|
||||
"""Create a new picture target.
|
||||
|
||||
@@ -138,7 +137,6 @@ class PictureTargetStore:
|
||||
adaptive_fps=adaptive_fps,
|
||||
protocol=protocol,
|
||||
description=description,
|
||||
auto_start=auto_start,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -150,7 +148,6 @@ class PictureTargetStore:
|
||||
picture_source_id=picture_source_id,
|
||||
settings=key_colors_settings or KeyColorsSettings(),
|
||||
description=description,
|
||||
auto_start=auto_start,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -178,7 +175,6 @@ class PictureTargetStore:
|
||||
protocol: Optional[str] = None,
|
||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||
description: Optional[str] = None,
|
||||
auto_start: Optional[bool] = None,
|
||||
) -> PictureTarget:
|
||||
"""Update a picture target.
|
||||
|
||||
@@ -209,7 +205,6 @@ class PictureTargetStore:
|
||||
protocol=protocol,
|
||||
key_colors_settings=key_colors_settings,
|
||||
description=description,
|
||||
auto_start=auto_start,
|
||||
)
|
||||
|
||||
target.updated_at = datetime.utcnow()
|
||||
|
||||
@@ -14,7 +14,6 @@ class TargetSnapshot:
|
||||
color_strip_source_id: str = ""
|
||||
brightness_value_source_id: str = ""
|
||||
fps: int = 30
|
||||
auto_start: bool = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -23,7 +22,6 @@ class TargetSnapshot:
|
||||
"color_strip_source_id": self.color_strip_source_id,
|
||||
"brightness_value_source_id": self.brightness_value_source_id,
|
||||
"fps": self.fps,
|
||||
"auto_start": self.auto_start,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -34,7 +32,6 @@ class TargetSnapshot:
|
||||
color_strip_source_id=data.get("color_strip_source_id", ""),
|
||||
brightness_value_source_id=data.get("brightness_value_source_id", ""),
|
||||
fps=data.get("fps", 30),
|
||||
auto_start=data.get("auto_start", False),
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +42,6 @@ class ScenePreset:
|
||||
id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
color: str = "#4fc3f7" # accent color for the card
|
||||
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||
order: int = 0
|
||||
created_at: datetime = field(default_factory=datetime.utcnow)
|
||||
@@ -56,7 +52,6 @@ class ScenePreset:
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"color": self.color,
|
||||
"targets": [t.to_dict() for t in self.targets],
|
||||
"order": self.order,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
@@ -69,7 +64,6 @@ class ScenePreset:
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
description=data.get("description", ""),
|
||||
color=data.get("color", "#4fc3f7"),
|
||||
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||
order=data.get("order", 0),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
|
||||
@@ -83,7 +83,6 @@ class ScenePresetStore:
|
||||
preset_id: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
order: Optional[int] = None,
|
||||
) -> ScenePreset:
|
||||
if preset_id not in self._presets:
|
||||
@@ -98,8 +97,6 @@ class ScenePresetStore:
|
||||
preset.name = name
|
||||
if description is not None:
|
||||
preset.description = description
|
||||
if color is not None:
|
||||
preset.color = color
|
||||
if order is not None:
|
||||
preset.order = order
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ class WledPictureTarget(PictureTarget):
|
||||
brightness_value_source_id=None,
|
||||
fps=None, keepalive_interval=None, state_check_interval=None,
|
||||
min_brightness_threshold=None, adaptive_fps=None, protocol=None,
|
||||
description=None, auto_start=None, **_kwargs) -> None:
|
||||
description=None, **_kwargs) -> None:
|
||||
"""Apply mutable field updates for WLED targets."""
|
||||
super().update_fields(name=name, description=description, auto_start=auto_start)
|
||||
super().update_fields(name=name, description=description)
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
if color_strip_source_id is not None:
|
||||
@@ -120,7 +120,6 @@ class WledPictureTarget(PictureTarget):
|
||||
adaptive_fps=data.get("adaptive_fps", False),
|
||||
protocol=data.get("protocol", "ddp"),
|
||||
description=data.get("description"),
|
||||
auto_start=data.get("auto_start", False),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
)
|
||||
|
||||
@@ -27,15 +27,6 @@
|
||||
<input type="text" id="scene-preset-editor-description">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="scene-preset-editor-color" data-i18n="scenes.color">Card Color:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="scenes.color.hint">Accent color for the scene card on the dashboard</small>
|
||||
<input type="color" id="scene-preset-editor-color" value="#4fc3f7">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="scene-target-selector-group" style="display:none">
|
||||
<div class="label-row">
|
||||
<label data-i18n="scenes.targets">Targets:</label>
|
||||
|
||||
Reference in New Issue
Block a user