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 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 14:38:25 +03:00
parent 466527bd4a
commit 701eac19e5
6 changed files with 101 additions and 38 deletions

View File

@@ -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;

View File

@@ -92,6 +92,9 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
condPills = `<span class="stream-card-prop">${t('profiles.conditions.empty')}</span>`;
} else {
const parts = profile.conditions.map(c => {
if (c.condition_type === 'always') {
return `<span class="stream-card-prop">✅ ${t('profiles.condition.always')}</span>`;
}
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 = `
<div class="condition-header">
<span class="condition-type-label">${t('profiles.condition.application')}</span>
<select class="condition-type-select">
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('profiles.condition.always')}</option>
<option value="application" ${condType === 'application' ? 'selected' : ''}>${t('profiles.condition.application')}</option>
</select>
<button type="button" class="btn-remove-condition" onclick="this.closest('.profile-condition-row').remove()" title="Remove">&#x2715;</button>
</div>
<div class="condition-fields">
<div class="condition-field">
<label data-i18n="profiles.condition.application.match_type">${t('profiles.condition.application.match_type')}</label>
<select class="condition-match-type">
<option value="running" ${matchType === 'running' ? 'selected' : ''}>${t('profiles.condition.application.match_type.running')}</option>
<option value="topmost" ${matchType === 'topmost' ? 'selected' : ''}>${t('profiles.condition.application.match_type.topmost')}</option>
<option value="topmost_fullscreen" ${matchType === 'topmost_fullscreen' ? 'selected' : ''}>${t('profiles.condition.application.match_type.topmost_fullscreen')}</option>
<option value="fullscreen" ${matchType === 'fullscreen' ? 'selected' : ''}>${t('profiles.condition.application.match_type.fullscreen')}</option>
</select>
</div>
<div class="condition-field">
<div class="condition-apps-header">
<label data-i18n="profiles.condition.application.apps">${t('profiles.condition.application.apps')}</label>
<button type="button" class="btn-browse-apps" title="${t('profiles.condition.application.browse')}">${t('profiles.condition.application.browse')}</button>
</div>
<textarea class="condition-apps" rows="3" placeholder="firefox.exe&#10;chrome.exe">${escapeHtml(appsValue)}</textarea>
<div class="process-picker" style="display:none">
<input type="text" class="process-picker-search" placeholder="${t('profiles.condition.application.search')}" autocomplete="off">
<div class="process-picker-list"></div>
</div>
</div>
</div>
<div class="condition-fields-container"></div>
`;
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 = `<small class="condition-always-desc">${t('profiles.condition.always.hint')}</small>`;
return;
}
const appsValue = (data.apps || []).join('\n');
const matchType = data.match_type || 'running';
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.application.match_type')}</label>
<select class="condition-match-type">
<option value="running" ${matchType === 'running' ? 'selected' : ''}>${t('profiles.condition.application.match_type.running')}</option>
<option value="topmost" ${matchType === 'topmost' ? 'selected' : ''}>${t('profiles.condition.application.match_type.topmost')}</option>
<option value="topmost_fullscreen" ${matchType === 'topmost_fullscreen' ? 'selected' : ''}>${t('profiles.condition.application.match_type.topmost_fullscreen')}</option>
<option value="fullscreen" ${matchType === 'fullscreen' ? 'selected' : ''}>${t('profiles.condition.application.match_type.fullscreen')}</option>
</select>
</div>
<div class="condition-field">
<div class="condition-apps-header">
<label>${t('profiles.condition.application.apps')}</label>
<button type="button" class="btn-browse-apps" title="${t('profiles.condition.application.browse')}">${t('profiles.condition.application.browse')}</button>
</div>
<textarea class="condition-apps" rows="3" placeholder="firefox.exe&#10;chrome.exe">${escapeHtml(appsValue)}</textarea>
<div class="process-picker" style="display:none">
<input type="text" class="process-picker-search" placeholder="${t('profiles.condition.application.search')}" autocomplete="off">
<div class="process-picker-list"></div>
</div>
</div>
</div>
`;
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;
}

View File

@@ -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)",

View File

@@ -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)",