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 = `
-
-
-
-
-
-
-
+
`;
- 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."""