From 701eac19e57969f816cf5827b45ea154f025ee21 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Wed, 25 Feb 2026 14:38:25 +0300 Subject: [PATCH] Add "Always" condition type to profiles - Add AlwaysCondition model and evaluation (always returns true) - Add condition type selector (Always/Application) in profile editor - Show condition type pill on profile cards - Fix misleading empty-conditions text (was "never activate", actually always active) - Add i18n keys for Always condition (en + ru) - Add CSS for condition type selector and description Co-Authored-By: Claude Opus 4.6 --- .../core/profiles/profile_engine.py | 4 +- .../wled_controller/static/css/profiles.css | 16 +++ .../static/js/features/profiles.js | 98 ++++++++++++------- .../wled_controller/static/locales/en.json | 4 +- .../wled_controller/static/locales/ru.json | 4 +- server/src/wled_controller/storage/profile.py | 13 +++ 6 files changed, 101 insertions(+), 38 deletions(-) diff --git a/server/src/wled_controller/core/profiles/profile_engine.py b/server/src/wled_controller/core/profiles/profile_engine.py index 34192ae..eb3d68c 100644 --- a/server/src/wled_controller/core/profiles/profile_engine.py +++ b/server/src/wled_controller/core/profiles/profile_engine.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from typing import Dict, Optional, Set from wled_controller.core.profiles.platform_detector import PlatformDetector -from wled_controller.storage.profile import ApplicationCondition, Condition, Profile +from wled_controller.storage.profile import AlwaysCondition, ApplicationCondition, Condition, Profile from wled_controller.storage.profile_store import ProfileStore from wled_controller.utils import get_logger @@ -158,6 +158,8 @@ class ProfileEngine: topmost_proc: Optional[str], topmost_fullscreen: bool, fullscreen_procs: Set[str], ) -> bool: + if isinstance(condition, AlwaysCondition): + return True if isinstance(condition, ApplicationCondition): return self._evaluate_app_condition(condition, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs) return False diff --git a/server/src/wled_controller/static/css/profiles.css b/server/src/wled_controller/static/css/profiles.css index 491d190..74829bd 100644 --- a/server/src/wled_controller/static/css/profiles.css +++ b/server/src/wled_controller/static/css/profiles.css @@ -48,6 +48,22 @@ font-size: 0.9rem; } +.condition-type-select { + font-weight: 600; + font-size: 0.9rem; + padding: 2px 6px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-color); + color: var(--text-color); +} + +.condition-always-desc { + display: block; + color: var(--text-muted); + font-size: 0.85rem; +} + .btn-remove-condition { background: none; border: none; diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/profiles.js index e3ad0ba..e402cfb 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/profiles.js @@ -92,6 +92,9 @@ function createProfileCard(profile, runningTargetIds = new Set()) { condPills = `${t('profiles.conditions.empty')}`; } else { const parts = profile.conditions.map(c => { + if (c.condition_type === 'always') { + return `✅ ${t('profiles.condition.always')}`; + } if (c.condition_type === 'application') { const apps = (c.apps || []).join(', '); const matchLabel = t('profiles.condition.application.match_type.' + (c.match_type || 'running')); @@ -233,45 +236,64 @@ function addProfileConditionRow(condition) { const list = document.getElementById('profile-conditions-list'); const row = document.createElement('div'); row.className = 'profile-condition-row'; - - const appsValue = (condition.apps || []).join('\n'); - const matchType = condition.match_type || 'running'; + const condType = condition.condition_type || 'application'; row.innerHTML = `
- ${t('profiles.condition.application')} +
-
-
- - -
-
-
- - -
- - -
-
+
`; - const browseBtn = row.querySelector('.btn-browse-apps'); - const picker = row.querySelector('.process-picker'); - browseBtn.addEventListener('click', () => toggleProcessPicker(picker, row)); + const typeSelect = row.querySelector('.condition-type-select'); + const container = row.querySelector('.condition-fields-container'); - const searchInput = row.querySelector('.process-picker-search'); - searchInput.addEventListener('input', () => filterProcessPicker(picker)); + function renderFields(type, data) { + if (type === 'always') { + container.innerHTML = `${t('profiles.condition.always.hint')}`; + return; + } + const appsValue = (data.apps || []).join('\n'); + const matchType = data.match_type || 'running'; + container.innerHTML = ` +
+
+ + +
+
+
+ + +
+ + +
+
+ `; + const browseBtn = container.querySelector('.btn-browse-apps'); + const picker = container.querySelector('.process-picker'); + browseBtn.addEventListener('click', () => toggleProcessPicker(picker, row)); + const searchInput = container.querySelector('.process-picker-search'); + searchInput.addEventListener('input', () => filterProcessPicker(picker)); + } + + renderFields(condType, condition); + typeSelect.addEventListener('change', () => { + renderFields(typeSelect.value, { apps: [], match_type: 'running' }); + }); list.appendChild(row); } @@ -340,10 +362,16 @@ function getProfileEditorConditions() { const rows = document.querySelectorAll('#profile-conditions-list .profile-condition-row'); const conditions = []; rows.forEach(row => { - const matchType = row.querySelector('.condition-match-type').value; - const appsText = row.querySelector('.condition-apps').value.trim(); - const apps = appsText ? appsText.split('\n').map(a => a.trim()).filter(Boolean) : []; - conditions.push({ condition_type: 'application', apps, match_type: matchType }); + const typeSelect = row.querySelector('.condition-type-select'); + const condType = typeSelect ? typeSelect.value : 'application'; + if (condType === 'always') { + conditions.push({ condition_type: 'always' }); + } else { + const matchType = row.querySelector('.condition-match-type').value; + const appsText = row.querySelector('.condition-apps').value.trim(); + const apps = appsText ? appsText.split('\n').map(a => a.trim()).filter(Boolean) : []; + conditions.push({ condition_type: 'application', apps, match_type: matchType }); + } }); return conditions; } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index b1b2552..56adc4e 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -519,7 +519,9 @@ "profiles.conditions": "Conditions:", "profiles.conditions.hint": "Rules that determine when this profile activates", "profiles.conditions.add": "Add Condition", - "profiles.conditions.empty": "No conditions \u2014 profile will never activate automatically", + "profiles.conditions.empty": "No conditions \u2014 profile is always active when enabled", + "profiles.condition.always": "Always", + "profiles.condition.always.hint": "Profile activates immediately when enabled and stays active. Use this to auto-start targets on server startup.", "profiles.condition.application": "Application", "profiles.condition.application.apps": "Applications:", "profiles.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 633f3ca..8f1d6f6 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -519,7 +519,9 @@ "profiles.conditions": "Условия:", "profiles.conditions.hint": "Правила, определяющие когда профиль активируется", "profiles.conditions.add": "Добавить условие", - "profiles.conditions.empty": "Нет условий \u2014 профиль не активируется автоматически", + "profiles.conditions.empty": "Нет условий \u2014 профиль всегда активен когда включён", + "profiles.condition.always": "Всегда", + "profiles.condition.always.hint": "Профиль активируется сразу при включении и остаётся активным. Используйте для автозапуска целей при старте сервера.", "profiles.condition.application": "Приложение", "profiles.condition.application.apps": "Приложения:", "profiles.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", diff --git a/server/src/wled_controller/storage/profile.py b/server/src/wled_controller/storage/profile.py index 3e6fb19..cd34718 100644 --- a/server/src/wled_controller/storage/profile.py +++ b/server/src/wled_controller/storage/profile.py @@ -18,11 +18,24 @@ class Condition: def from_dict(cls, data: dict) -> "Condition": """Factory: dispatch to the correct subclass.""" ct = data.get("condition_type", "") + if ct == "always": + return AlwaysCondition.from_dict(data) if ct == "application": return ApplicationCondition.from_dict(data) raise ValueError(f"Unknown condition type: {ct}") +@dataclass +class AlwaysCondition(Condition): + """Always-true condition — profile activates unconditionally when enabled.""" + + condition_type: str = "always" + + @classmethod + def from_dict(cls, data: dict) -> "AlwaysCondition": + return cls() + + @dataclass class ApplicationCondition(Condition): """Activate when specified applications are running or topmost."""