From fddbd771f2082df59243a81c750df602a98b4534 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 2 Mar 2026 01:09:27 +0300 Subject: [PATCH] Replace auto-start with startup automation, add card colors to dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../wled_controller/api/routes/automations.py | 3 + .../api/routes/picture_targets.py | 8 +- .../api/routes/scene_presets.py | 4 - .../api/schemas/picture_targets.py | 3 - .../api/schemas/scene_presets.py | 4 - .../core/automations/automation_engine.py | 3 +- .../core/scenes/scene_activator.py | 4 - server/src/wled_controller/main.py | 13 --- server/src/wled_controller/static/js/app.js | 6 +- .../static/js/core/card-colors.js | 5 +- .../static/js/features/automations.js | 10 +++ .../static/js/features/dashboard.js | 90 +++---------------- .../static/js/features/kc-targets.js | 1 - .../static/js/features/scene-presets.js | 19 ++-- .../static/js/features/targets.js | 20 ----- .../static/js/features/tutorials.js | 3 +- .../wled_controller/static/locales/en.json | 13 +-- .../wled_controller/static/locales/ru.json | 13 +-- .../wled_controller/static/locales/zh.json | 13 +-- server/src/wled_controller/static/sw.js | 2 +- .../src/wled_controller/storage/automation.py | 13 +++ .../storage/key_colors_picture_target.py | 5 +- .../wled_controller/storage/picture_target.py | 6 +- .../storage/picture_target_store.py | 5 -- .../wled_controller/storage/scene_preset.py | 6 -- .../storage/scene_preset_store.py | 3 - .../storage/wled_picture_target.py | 5 +- .../templates/modals/scene-preset-editor.html | 9 -- 28 files changed, 78 insertions(+), 211 deletions(-) diff --git a/server/src/wled_controller/api/routes/automations.py b/server/src/wled_controller/api/routes/automations.py index 44370c9..1a1d332 100644 --- a/server/src/wled_controller/api/routes/automations.py +++ b/server/src/wled_controller/api/routes/automations.py @@ -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}") diff --git a/server/src/wled_controller/api/routes/picture_targets.py b/server/src/wled_controller/api/routes/picture_targets.py index 8b1d9c2..ac46902 100644 --- a/server/src/wled_controller/api/routes/picture_targets.py +++ b/server/src/wled_controller/api/routes/picture_targets.py @@ -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) diff --git a/server/src/wled_controller/api/routes/scene_presets.py b/server/src/wled_controller/api/routes/scene_presets.py index 58e4bd7..1df32ca 100644 --- a/server/src/wled_controller/api/routes/scene_presets.py +++ b/server/src/wled_controller/api/routes/scene_presets.py @@ -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: diff --git a/server/src/wled_controller/api/schemas/picture_targets.py b/server/src/wled_controller/api/schemas/picture_targets.py index 52d8f65..0f72456 100644 --- a/server/src/wled_controller/api/schemas/picture_targets.py +++ b/server/src/wled_controller/api/schemas/picture_targets.py @@ -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") diff --git a/server/src/wled_controller/api/schemas/scene_presets.py b/server/src/wled_controller/api/schemas/scene_presets.py index 7ed57ca..467ae92 100644 --- a/server/src/wled_controller/api/schemas/scene_presets.py +++ b/server/src/wled_controller/api/schemas/scene_presets.py @@ -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 diff --git a/server/src/wled_controller/core/automations/automation_engine.py b/server/src/wled_controller/core/automations/automation_engine.py index 05686a3..09fa3c4 100644 --- a/server/src/wled_controller/core/automations/automation_engine.py +++ b/server/src/wled_controller/core/automations/automation_engine.py @@ -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) diff --git a/server/src/wled_controller/core/scenes/scene_activator.py b/server/src/wled_controller/core/scenes/scene_activator.py index 3f68e35..ca0a113 100644 --- a/server/src/wled_controller/core/scenes/scene_activator.py +++ b/server/src/wled_controller/core/scenes/scene_activator.py @@ -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) diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 3d43c8d..6031d7f 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -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 diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 0fba888..f1865e4 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -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 diff --git a/server/src/wled_controller/static/js/core/card-colors.js b/server/src/wled_controller/static/js/core/card-colors.js index 52c9f9d..38a441b 100644 --- a/server/src/wled_controller/static/js/core/card-colors.js +++ b/server/src/wled_controller/static/js/core/card-colors.js @@ -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}` : ''; }); diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js index 0d163d9..9627971 100644 --- a/server/src/wled_controller/static/js/features/automations.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -113,6 +113,9 @@ function createAutomationCard(automation, sceneMap = new Map()) { if (c.condition_type === 'always') { return `${ICON_OK} ${t('automations.condition.always')}`; } + if (c.condition_type === 'startup') { + return `${ICON_START} ${t('automations.condition.startup')}`; + } 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) {
-
-
- - -
- - -
-