Rename profiles to automations across backend and frontend

Rename the "profiles" entity to "automations" throughout the entire
codebase for clarity. Updates Python models, storage, API routes/schemas,
engine, frontend JS modules, HTML templates, CSS classes, i18n keys
(en/ru/zh), dashboard, tutorials, and command palette.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 18:01:39 +03:00
parent da3e53e1f1
commit 21248e2dc9
39 changed files with 1180 additions and 1179 deletions

View File

@@ -1,34 +1,34 @@
/* ===== PROFILES ===== */
/* ===== AUTOMATIONS ===== */
.badge-profile-active {
.badge-automation-active {
background: var(--success-color);
color: #fff;
}
.badge-profile-inactive {
.badge-automation-inactive {
background: var(--border-color);
color: var(--text-color);
}
.badge-profile-disabled {
.badge-automation-disabled {
background: var(--border-color);
color: var(--text-muted);
opacity: 0.7;
}
.profile-status-disabled {
.automation-status-disabled {
opacity: 0.6;
}
.profile-logic-label {
.automation-logic-label {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-muted);
padding: 0 4px;
}
/* Profile condition editor rows */
.profile-condition-row {
/* Automation condition editor rows */
.automation-condition-row {
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;

View File

@@ -294,7 +294,7 @@
flex-shrink: 0;
}
.dashboard-profile .dashboard-target-metrics {
.dashboard-automation .dashboard-target-metrics {
min-width: 48px;
}

View File

@@ -23,11 +23,11 @@ import {
} from './features/displays.js';
import {
startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial,
startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startProfilesTutorial,
startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial,
closeTutorial, tutorialNext, tutorialPrev,
} from './features/tutorials.js';
// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, profiles
// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, automations
import {
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
@@ -36,7 +36,7 @@ import {
} from './features/devices.js';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleProfile, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.js';
import { startEventsWS, stopEventsWS } from './core/events-ws.js';
@@ -77,11 +77,11 @@ import {
clonePatternTemplate,
} from './features/pattern-templates.js';
import {
loadProfiles, openProfileEditor, closeProfileEditorModal,
saveProfileEditor, addProfileCondition,
toggleProfileEnabled, deleteProfile,
expandAllProfileSections, collapseAllProfileSections,
} from './features/profiles.js';
loadAutomations, openAutomationEditor, closeAutomationEditorModal,
saveAutomationEditor, addAutomationCondition,
toggleAutomationEnabled, deleteAutomation,
expandAllAutomationSections, collapseAllAutomationSections,
} from './features/automations.js';
import {
loadScenes, expandAllSceneSections, collapseAllSceneSections,
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
@@ -185,7 +185,7 @@ Object.assign(window, {
startDashboardTutorial,
startTargetsTutorial,
startSourcesTutorial,
startProfilesTutorial,
startAutomationsTutorial,
closeTutorial,
tutorialNext,
tutorialPrev,
@@ -204,7 +204,7 @@ Object.assign(window, {
// dashboard
loadDashboard,
dashboardToggleProfile,
dashboardToggleAutomation,
dashboardStartTarget,
dashboardStopTarget,
dashboardToggleAutoStart,
@@ -300,16 +300,16 @@ Object.assign(window, {
capturePatternBackground,
clonePatternTemplate,
// profiles
loadProfiles,
openProfileEditor,
closeProfileEditorModal,
saveProfileEditor,
addProfileCondition,
toggleProfileEnabled,
deleteProfile,
expandAllProfileSections,
collapseAllProfileSections,
// automations
loadAutomations,
openAutomationEditor,
closeAutomationEditorModal,
saveAutomationEditor,
addAutomationCondition,
toggleAutomationEnabled,
deleteAutomation,
expandAllAutomationSections,
collapseAllAutomationSections,
// scene presets
loadScenes,
@@ -440,7 +440,7 @@ document.addEventListener('keydown', (e) => {
// Tab shortcuts: Ctrl+1..5 (skip when typing in inputs)
if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
const tabMap = { '1': 'dashboard', '2': 'profiles', '3': 'targets', '4': 'streams', '5': 'scenes' };
const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'scenes' };
const tab = tabMap[e.key];
if (tab) {
e.preventDefault();

View File

@@ -7,7 +7,7 @@ import { t } from './i18n.js';
import { navigateToCard } from './navigation.js';
import {
getTargetTypeIcon, getPictureSourceIcon, getColorStripIcon, getAudioSourceIcon,
ICON_DEVICE, ICON_TARGET, ICON_PROFILE, ICON_VALUE_SOURCE, ICON_SCENE,
ICON_DEVICE, ICON_TARGET, ICON_AUTOMATION, ICON_VALUE_SOURCE, ICON_SCENE,
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_PATTERN_TEMPLATE,
} from './icons.js';
@@ -31,7 +31,7 @@ function _mapEntities(data, mapFn) {
}
function _buildItems(results) {
const [devices, targets, css, profiles, capTempl, ppTempl, patTempl, audioSrc, valSrc, streams, scenePresets] = results;
const [devices, targets, css, automations, capTempl, ppTempl, patTempl, audioSrc, valSrc, streams, scenePresets] = results;
const items = [];
_mapEntities(devices, d => items.push({
@@ -58,9 +58,9 @@ function _buildItems(results) {
nav: ['targets', 'led', 'led-css', 'data-css-id', c.id],
}));
_mapEntities(profiles, p => items.push({
name: p.name, detail: p.enabled ? 'enabled' : '', group: 'profiles', icon: ICON_PROFILE,
nav: ['profiles', null, 'profiles', 'data-profile-id', p.id],
_mapEntities(automations, a => items.push({
name: a.name, detail: a.enabled ? 'enabled' : '', group: 'automations', icon: ICON_AUTOMATION,
nav: ['automations', null, 'automations', 'data-automation-id', a.id],
}));
_mapEntities(capTempl, ct => items.push({
@@ -112,7 +112,7 @@ const _responseKeys = [
['/devices', 'devices'],
['/picture-targets', 'targets'],
['/color-strip-sources', 'sources'],
['/profiles', 'profiles'],
['/automations', 'automations'],
['/capture-templates', 'templates'],
['/postprocessing-templates','templates'],
['/pattern-templates', 'templates'],
@@ -136,7 +136,7 @@ async function _fetchAllEntities() {
// ─── Group ordering ───
const _groupOrder = [
'devices', 'targets', 'kc_targets', 'css', 'profiles',
'devices', 'targets', 'kc_targets', 'css', 'automations',
'streams', 'capture_templates', 'pp_templates', 'pattern_templates',
'audio', 'value', 'scenes',
];

View File

@@ -2,7 +2,7 @@
* Global events WebSocket — stays connected while logged in,
* dispatches DOM custom events that feature modules can listen to.
*
* Events dispatched: server:state_change, server:profile_state_changed
* Events dispatched: server:state_change, server:automation_state_changed
*/
import { apiKey } from './state.js';

View File

@@ -64,7 +64,7 @@ export function getEngineIcon(engineType) {
// ── Entity-kind constants ───────────────────────────────────
export const ICON_PROFILE = _svg(P.clipboardList);
export const ICON_AUTOMATION = _svg(P.clipboardList);
export const ICON_DEVICE = _svg(P.monitor);
export const ICON_TARGET = _svg(P.zap);
export const ICON_VALUE_SOURCE = _svg(P.hash);

View File

@@ -7,7 +7,7 @@ import { switchTab } from '../features/tabs.js';
/**
* Navigate to a card on any tab/subtab, expanding the section and scrolling to it.
*
* @param {string} tab Main tab: 'dashboard' | 'profiles' | 'targets' | 'streams'
* @param {string} tab Main tab: 'dashboard' | 'automations' | 'targets' | 'streams'
* @param {string|null} subTab Sub-tab key or null
* @param {string|null} sectionKey CardSection key to expand, or null
* @param {string} cardAttr Data attribute to find the card (e.g. 'data-device-id')
@@ -87,7 +87,7 @@ function _highlightCard(card) {
/** Trigger the tab's data load function (used when card wasn't found in DOM). */
function _triggerTabLoad(tab) {
if (tab === 'dashboard' && typeof window.loadDashboard === 'function') window.loadDashboard();
else if (tab === 'profiles' && typeof window.loadProfiles === 'function') window.loadProfiles();
else if (tab === 'automations' && typeof window.loadAutomations === 'function') window.loadAutomations();
else if (tab === 'streams' && typeof window.loadPictureSources === 'function') window.loadPictureSources();
else if (tab === 'targets' && typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
else if (tab === 'scenes' && typeof window.loadScenes === 'function') window.loadScenes();

View File

@@ -128,8 +128,8 @@ export function set_dashboardLoading(v) { _dashboardLoading = v; }
export let _sourcesLoading = false;
export function set_sourcesLoading(v) { _sourcesLoading = v; }
export let _profilesLoading = false;
export function set_profilesLoading(v) { _profilesLoading = v; }
export let _automationsLoading = false;
export function set_automationsLoading(v) { _automationsLoading = v; }
// Dashboard poll interval (ms), persisted in localStorage
const _POLL_KEY = 'dashboard_poll_interval';
@@ -195,6 +195,6 @@ export function set_audioTemplateNameManuallyEdited(v) { _audioTemplateNameManua
export let _cachedValueSources = [];
export function set_cachedValueSources(v) { _cachedValueSources = v; }
// Profiles
export let _profilesCache = null;
export function set_profilesCache(v) { _profilesCache = v; }
// Automations
export let _automationsCache = null;
export function set_automationsCache(v) { _automationsCache = v; }

View File

@@ -1,199 +1,199 @@
/**
* Profiles profile cards, editor, condition builder, process picker, scene selector.
* Automations automation cards, editor, condition builder, process picker, scene selector.
*/
import { apiKey, _profilesCache, set_profilesCache, _profilesLoading, set_profilesLoading } from '../core/state.js';
import { apiKey, _automationsCache, set_automationsCache, _automationsLoading, set_automationsLoading } from '../core/state.js';
import { fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js';
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { CardSection } from '../core/card-sections.js';
import { updateTabBadge } from './tabs.js';
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_PROFILE, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
// ===== Scene presets cache (shared by both selectors) =====
let _scenesCache = [];
class ProfileEditorModal extends Modal {
constructor() { super('profile-editor-modal'); }
class AutomationEditorModal extends Modal {
constructor() { super('automation-editor-modal'); }
snapshotValues() {
return {
name: document.getElementById('profile-editor-name').value,
enabled: document.getElementById('profile-editor-enabled').checked.toString(),
logic: document.getElementById('profile-editor-logic').value,
conditions: JSON.stringify(getProfileEditorConditions()),
scenePresetId: document.getElementById('profile-scene-id').value,
deactivationMode: document.getElementById('profile-deactivation-mode').value,
deactivationScenePresetId: document.getElementById('profile-fallback-scene-id').value,
name: document.getElementById('automation-editor-name').value,
enabled: document.getElementById('automation-editor-enabled').checked.toString(),
logic: document.getElementById('automation-editor-logic').value,
conditions: JSON.stringify(getAutomationEditorConditions()),
scenePresetId: document.getElementById('automation-scene-id').value,
deactivationMode: document.getElementById('automation-deactivation-mode').value,
deactivationScenePresetId: document.getElementById('automation-fallback-scene-id').value,
};
}
}
const profileModal = new ProfileEditorModal();
const csProfiles = new CardSection('profiles', { titleKey: 'profiles.title', gridClass: 'devices-grid', addCardOnclick: "openProfileEditor()", keyAttr: 'data-profile-id' });
const automationModal = new AutomationEditorModal();
const csAutomations = new CardSection('automations', { titleKey: 'automations.title', gridClass: 'devices-grid', addCardOnclick: "openAutomationEditor()", keyAttr: 'data-automation-id' });
// Re-render profiles when language changes (only if tab is active)
// Re-render automations when language changes (only if tab is active)
document.addEventListener('languageChanged', () => {
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'profiles') loadProfiles();
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') loadAutomations();
});
// React to real-time profile state changes from global events WS
document.addEventListener('server:profile_state_changed', () => {
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'profiles') {
loadProfiles();
// React to real-time automation state changes from global events WS
document.addEventListener('server:automation_state_changed', () => {
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') {
loadAutomations();
}
});
export async function loadProfiles() {
if (_profilesLoading) return;
set_profilesLoading(true);
const container = document.getElementById('profiles-content');
if (!container) { set_profilesLoading(false); return; }
setTabRefreshing('profiles-content', true);
export async function loadAutomations() {
if (_automationsLoading) return;
set_automationsLoading(true);
const container = document.getElementById('automations-content');
if (!container) { set_automationsLoading(false); return; }
setTabRefreshing('automations-content', true);
try {
const [profilesResp, scenesResp] = await Promise.all([
fetchWithAuth('/profiles'),
const [automationsResp, scenesResp] = await Promise.all([
fetchWithAuth('/automations'),
fetchWithAuth('/scene-presets'),
]);
if (!profilesResp.ok) throw new Error('Failed to load profiles');
const data = await profilesResp.json();
if (!automationsResp.ok) throw new Error('Failed to load automations');
const data = await automationsResp.json();
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
_scenesCache = scenesData.presets || [];
// Build scene name map for card rendering
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
set_profilesCache(data.profiles);
const activeCount = data.profiles.filter(p => p.is_active).length;
updateTabBadge('profiles', activeCount);
renderProfiles(data.profiles, sceneMap);
set_automationsCache(data.automations);
const activeCount = data.automations.filter(a => a.is_active).length;
updateTabBadge('automations', activeCount);
renderAutomations(data.automations, sceneMap);
} catch (error) {
if (error.isAuth) return;
console.error('Failed to load profiles:', error);
console.error('Failed to load automations:', error);
container.innerHTML = `<p class="error-message">${error.message}</p>`;
} finally {
set_profilesLoading(false);
setTabRefreshing('profiles-content', false);
set_automationsLoading(false);
setTabRefreshing('automations-content', false);
}
}
export function expandAllProfileSections() {
CardSection.expandAll([csProfiles]);
export function expandAllAutomationSections() {
CardSection.expandAll([csAutomations]);
}
export function collapseAllProfileSections() {
CardSection.collapseAll([csProfiles]);
export function collapseAllAutomationSections() {
CardSection.collapseAll([csAutomations]);
}
function renderProfiles(profiles, sceneMap) {
const container = document.getElementById('profiles-content');
function renderAutomations(automations, sceneMap) {
const container = document.getElementById('automations-content');
const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, sceneMap) })));
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllProfileSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllProfileSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startProfilesTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
container.innerHTML = toolbar + csProfiles.render(items);
csProfiles.bind();
const items = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
container.innerHTML = toolbar + csAutomations.render(items);
csAutomations.bind();
// Localize data-i18n elements within the profiles container only
// Localize data-i18n elements within the automations container only
container.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
}
function createProfileCard(profile, sceneMap = new Map()) {
const statusClass = !profile.enabled ? 'disabled' : profile.is_active ? 'active' : 'inactive';
const statusText = !profile.enabled ? t('profiles.status.disabled') : profile.is_active ? t('profiles.status.active') : t('profiles.status.inactive');
function createAutomationCard(automation, sceneMap = new Map()) {
const statusClass = !automation.enabled ? 'disabled' : automation.is_active ? 'active' : 'inactive';
const statusText = !automation.enabled ? t('automations.status.disabled') : automation.is_active ? t('automations.status.active') : t('automations.status.inactive');
let condPills = '';
if (profile.conditions.length === 0) {
condPills = `<span class="stream-card-prop">${t('profiles.conditions.empty')}</span>`;
if (automation.conditions.length === 0) {
condPills = `<span class="stream-card-prop">${t('automations.conditions.empty')}</span>`;
} else {
const parts = profile.conditions.map(c => {
const parts = automation.conditions.map(c => {
if (c.condition_type === 'always') {
return `<span class="stream-card-prop">${ICON_OK} ${t('profiles.condition.always')}</span>`;
return `<span class="stream-card-prop">${ICON_OK} ${t('automations.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'));
return `<span class="stream-card-prop stream-card-prop-full">${t('profiles.condition.application')}: ${apps} (${matchLabel})</span>`;
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
return `<span class="stream-card-prop stream-card-prop-full">${t('automations.condition.application')}: ${apps} (${matchLabel})</span>`;
}
if (c.condition_type === 'time_of_day') {
return `<span class="stream-card-prop">${ICON_CLOCK} ${c.start_time || '00:00'} ${c.end_time || '23:59'}</span>`;
}
if (c.condition_type === 'system_idle') {
const mode = c.when_idle !== false ? t('profiles.condition.system_idle.when_idle') : t('profiles.condition.system_idle.when_active');
const mode = c.when_idle !== false ? t('automations.condition.system_idle.when_idle') : t('automations.condition.system_idle.when_active');
return `<span class="stream-card-prop">${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})</span>`;
}
if (c.condition_type === 'display_state') {
const stateLabel = t('profiles.condition.display_state.' + (c.state || 'on'));
return `<span class="stream-card-prop">${ICON_MONITOR} ${t('profiles.condition.display_state')}: ${stateLabel}</span>`;
const stateLabel = t('automations.condition.display_state.' + (c.state || 'on'));
return `<span class="stream-card-prop">${ICON_MONITOR} ${t('automations.condition.display_state')}: ${stateLabel}</span>`;
}
if (c.condition_type === 'mqtt') {
return `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('profiles.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`;
return `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`;
}
return `<span class="stream-card-prop">${c.condition_type}</span>`;
});
const logicLabel = profile.condition_logic === 'and' ? t('profiles.logic.and') : t('profiles.logic.or');
condPills = parts.join(`<span class="profile-logic-label">${logicLabel}</span>`);
const logicLabel = automation.condition_logic === 'and' ? t('automations.logic.and') : t('automations.logic.or');
condPills = parts.join(`<span class="automation-logic-label">${logicLabel}</span>`);
}
// Scene info
const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null;
const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected');
const scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null;
const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected');
const sceneColor = scene ? scene.color || '#4fc3f7' : '#888';
// Deactivation mode label
let deactivationLabel = '';
if (profile.deactivation_mode === 'revert') {
deactivationLabel = t('profiles.deactivation_mode.revert');
} else if (profile.deactivation_mode === 'fallback_scene') {
const fallback = profile.deactivation_scene_preset_id ? sceneMap.get(profile.deactivation_scene_preset_id) : null;
deactivationLabel = fallback ? `${t('profiles.deactivation_mode.fallback_scene')}: ${escapeHtml(fallback.name)}` : t('profiles.deactivation_mode.fallback_scene');
if (automation.deactivation_mode === 'revert') {
deactivationLabel = t('automations.deactivation_mode.revert');
} else if (automation.deactivation_mode === 'fallback_scene') {
const fallback = automation.deactivation_scene_preset_id ? sceneMap.get(automation.deactivation_scene_preset_id) : null;
deactivationLabel = fallback ? `${t('automations.deactivation_mode.fallback_scene')}: ${escapeHtml(fallback.name)}` : t('automations.deactivation_mode.fallback_scene');
}
let lastActivityMeta = '';
if (profile.last_activated_at) {
const ts = new Date(profile.last_activated_at);
lastActivityMeta = `<span class="card-meta" title="${t('profiles.last_activated')}">${ICON_CLOCK} ${ts.toLocaleString()}</span>`;
if (automation.last_activated_at) {
const ts = new Date(automation.last_activated_at);
lastActivityMeta = `<span class="card-meta" title="${t('automations.last_activated')}">${ICON_CLOCK} ${ts.toLocaleString()}</span>`;
}
return `
<div class="card${!profile.enabled ? ' profile-status-disabled' : ''}" data-profile-id="${profile.id}">
<div class="card${!automation.enabled ? ' automation-status-disabled' : ''}" data-automation-id="${automation.id}">
<div class="card-top-actions">
<button class="card-remove-btn" onclick="deleteProfile('${profile.id}', '${escapeHtml(profile.name)}')" title="${t('common.delete')}">&#x2715;</button>
<button class="card-remove-btn" onclick="deleteAutomation('${automation.id}', '${escapeHtml(automation.name)}')" title="${t('common.delete')}">&#x2715;</button>
</div>
<div class="card-header">
<div class="card-title">
${escapeHtml(profile.name)}
<span class="badge badge-profile-${statusClass}">${statusText}</span>
${escapeHtml(automation.name)}
<span class="badge badge-automation-${statusClass}">${statusText}</span>
</div>
</div>
<div class="card-subtitle">
<span class="card-meta">${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')}</span>
<span class="card-meta">${automation.condition_logic === 'and' ? t('automations.logic.all') : t('automations.logic.any')}</span>
<span class="card-meta">${ICON_SCENE} <span style="color:${sceneColor}">&#x25CF;</span> ${sceneName}</span>
${deactivationLabel ? `<span class="card-meta">${deactivationLabel}</span>` : ''}
${lastActivityMeta}
</div>
<div class="stream-card-props">${condPills}</div>
<div class="card-actions">
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">${ICON_SETTINGS}</button>
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
${profile.enabled ? ICON_PAUSE : ICON_START}
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
<button class="btn btn-icon ${automation.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleAutomationEnabled('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
${automation.enabled ? ICON_PAUSE : ICON_START}
</button>
</div>
</div>`;
}
export async function openProfileEditor(profileId) {
const modal = document.getElementById('profile-editor-modal');
const titleEl = document.getElementById('profile-editor-title');
const idInput = document.getElementById('profile-editor-id');
const nameInput = document.getElementById('profile-editor-name');
const enabledInput = document.getElementById('profile-editor-enabled');
const logicSelect = document.getElementById('profile-editor-logic');
const condList = document.getElementById('profile-conditions-list');
const errorEl = document.getElementById('profile-editor-error');
export async function openAutomationEditor(automationId) {
const modal = document.getElementById('automation-editor-modal');
const titleEl = document.getElementById('automation-editor-title');
const idInput = document.getElementById('automation-editor-id');
const nameInput = document.getElementById('automation-editor-name');
const enabledInput = document.getElementById('automation-editor-enabled');
const logicSelect = document.getElementById('automation-editor-logic');
const condList = document.getElementById('automation-conditions-list');
const errorEl = document.getElementById('automation-editor-error');
errorEl.style.display = 'none';
condList.innerHTML = '';
@@ -208,66 +208,66 @@ export async function openProfileEditor(profileId) {
} catch { /* use cached */ }
// Reset deactivation mode
document.getElementById('profile-deactivation-mode').value = 'none';
document.getElementById('profile-fallback-scene-group').style.display = 'none';
document.getElementById('automation-deactivation-mode').value = 'none';
document.getElementById('automation-fallback-scene-group').style.display = 'none';
if (profileId) {
titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.edit')}`;
if (automationId) {
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.edit')}`;
try {
const resp = await fetchWithAuth(`/profiles/${profileId}`);
if (!resp.ok) throw new Error('Failed to load profile');
const profile = await resp.json();
const resp = await fetchWithAuth(`/automations/${automationId}`);
if (!resp.ok) throw new Error('Failed to load automation');
const automation = await resp.json();
idInput.value = profile.id;
nameInput.value = profile.name;
enabledInput.checked = profile.enabled;
logicSelect.value = profile.condition_logic;
idInput.value = automation.id;
nameInput.value = automation.name;
enabledInput.checked = automation.enabled;
logicSelect.value = automation.condition_logic;
for (const c of profile.conditions) {
addProfileConditionRow(c);
for (const c of automation.conditions) {
addAutomationConditionRow(c);
}
// Scene selector
_initSceneSelector('profile-scene', profile.scene_preset_id);
_initSceneSelector('automation-scene', automation.scene_preset_id);
// Deactivation mode
document.getElementById('profile-deactivation-mode').value = profile.deactivation_mode || 'none';
document.getElementById('automation-deactivation-mode').value = automation.deactivation_mode || 'none';
_onDeactivationModeChange();
_initSceneSelector('profile-fallback-scene', profile.deactivation_scene_preset_id);
_initSceneSelector('automation-fallback-scene', automation.deactivation_scene_preset_id);
} catch (e) {
showToast(e.message, 'error');
return;
}
} else {
titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.add')}`;
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
idInput.value = '';
nameInput.value = '';
enabledInput.checked = true;
logicSelect.value = 'or';
_initSceneSelector('profile-scene', null);
_initSceneSelector('profile-fallback-scene', null);
_initSceneSelector('automation-scene', null);
_initSceneSelector('automation-fallback-scene', null);
}
// Wire up deactivation mode change
document.getElementById('profile-deactivation-mode').onchange = _onDeactivationModeChange;
document.getElementById('automation-deactivation-mode').onchange = _onDeactivationModeChange;
profileModal.open();
automationModal.open();
modal.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
modal.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
});
profileModal.snapshot();
automationModal.snapshot();
}
function _onDeactivationModeChange() {
const mode = document.getElementById('profile-deactivation-mode').value;
document.getElementById('profile-fallback-scene-group').style.display = mode === 'fallback_scene' ? '' : 'none';
const mode = document.getElementById('automation-deactivation-mode').value;
document.getElementById('automation-fallback-scene-group').style.display = mode === 'fallback_scene' ? '' : 'none';
}
export async function closeProfileEditorModal() {
await profileModal.close();
export async function closeAutomationEditorModal() {
await automationModal.close();
}
// ===== Scene selector logic =====
@@ -296,7 +296,7 @@ function _initSceneSelector(prefix, selectedId) {
const filtered = query ? _scenesCache.filter(s => s.name.toLowerCase().includes(query)) : _scenesCache;
if (filtered.length === 0) {
dropdown.innerHTML = `<div class="scene-selector-empty">${t('profiles.scene.none_available')}</div>`;
dropdown.innerHTML = `<div class="scene-selector-empty">${t('automations.scene.none_available')}</div>`;
} else {
dropdown.innerHTML = filtered.map(s => {
const selected = s.id === hiddenInput.value ? ' selected' : '';
@@ -370,27 +370,27 @@ function _initSceneSelector(prefix, selectedId) {
// ===== Condition editor =====
export function addProfileCondition() {
addProfileConditionRow({ condition_type: 'application', apps: [], match_type: 'running' });
export function addAutomationCondition() {
addAutomationConditionRow({ condition_type: 'application', apps: [], match_type: 'running' });
}
function addProfileConditionRow(condition) {
const list = document.getElementById('profile-conditions-list');
function addAutomationConditionRow(condition) {
const list = document.getElementById('automation-conditions-list');
const row = document.createElement('div');
row.className = 'profile-condition-row';
row.className = 'automation-condition-row';
const condType = condition.condition_type || 'application';
row.innerHTML = `
<div class="condition-header">
<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>
<option value="time_of_day" ${condType === 'time_of_day' ? 'selected' : ''}>${t('profiles.condition.time_of_day')}</option>
<option value="system_idle" ${condType === 'system_idle' ? 'selected' : ''}>${t('profiles.condition.system_idle')}</option>
<option value="display_state" ${condType === 'display_state' ? 'selected' : ''}>${t('profiles.condition.display_state')}</option>
<option value="mqtt" ${condType === 'mqtt' ? 'selected' : ''}>${t('profiles.condition.mqtt')}</option>
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('automations.condition.always')}</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>
<option value="display_state" ${condType === 'display_state' ? 'selected' : ''}>${t('automations.condition.display_state')}</option>
<option value="mqtt" ${condType === 'mqtt' ? 'selected' : ''}>${t('automations.condition.mqtt')}</option>
</select>
<button type="button" class="btn-remove-condition" onclick="this.closest('.profile-condition-row').remove()" title="Remove">&#x2715;</button>
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove()" title="Remove">&#x2715;</button>
</div>
<div class="condition-fields-container"></div>
`;
@@ -400,7 +400,7 @@ function addProfileConditionRow(condition) {
function renderFields(type, data) {
if (type === 'always') {
container.innerHTML = `<small class="condition-always-desc">${t('profiles.condition.always.hint')}</small>`;
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.always.hint')}</small>`;
return;
}
if (type === 'time_of_day') {
@@ -409,14 +409,14 @@ function addProfileConditionRow(condition) {
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.time_of_day.start_time')}</label>
<label>${t('automations.condition.time_of_day.start_time')}</label>
<input type="time" class="condition-start-time" value="${startTime}">
</div>
<div class="condition-field">
<label>${t('profiles.condition.time_of_day.end_time')}</label>
<label>${t('automations.condition.time_of_day.end_time')}</label>
<input type="time" class="condition-end-time" value="${endTime}">
</div>
<small class="condition-always-desc">${t('profiles.condition.time_of_day.overnight_hint')}</small>
<small class="condition-always-desc">${t('automations.condition.time_of_day.overnight_hint')}</small>
</div>`;
return;
}
@@ -426,14 +426,14 @@ function addProfileConditionRow(condition) {
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.system_idle.idle_minutes')}</label>
<label>${t('automations.condition.system_idle.idle_minutes')}</label>
<input type="number" class="condition-idle-minutes" min="1" max="999" value="${idleMinutes}">
</div>
<div class="condition-field">
<label>${t('profiles.condition.system_idle.mode')}</label>
<label>${t('automations.condition.system_idle.mode')}</label>
<select class="condition-when-idle">
<option value="true" ${whenIdle ? 'selected' : ''}>${t('profiles.condition.system_idle.when_idle')}</option>
<option value="false" ${!whenIdle ? 'selected' : ''}>${t('profiles.condition.system_idle.when_active')}</option>
<option value="true" ${whenIdle ? 'selected' : ''}>${t('automations.condition.system_idle.when_idle')}</option>
<option value="false" ${!whenIdle ? 'selected' : ''}>${t('automations.condition.system_idle.when_active')}</option>
</select>
</div>
</div>`;
@@ -444,10 +444,10 @@ function addProfileConditionRow(condition) {
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.display_state.state')}</label>
<label>${t('automations.condition.display_state.state')}</label>
<select class="condition-display-state">
<option value="on" ${dState === 'on' ? 'selected' : ''}>${t('profiles.condition.display_state.on')}</option>
<option value="off" ${dState === 'off' ? 'selected' : ''}>${t('profiles.condition.display_state.off')}</option>
<option value="on" ${dState === 'on' ? 'selected' : ''}>${t('automations.condition.display_state.on')}</option>
<option value="off" ${dState === 'off' ? 'selected' : ''}>${t('automations.condition.display_state.off')}</option>
</select>
</div>
</div>`;
@@ -460,19 +460,19 @@ function addProfileConditionRow(condition) {
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.mqtt.topic')}</label>
<label>${t('automations.condition.mqtt.topic')}</label>
<input type="text" class="condition-mqtt-topic" value="${escapeHtml(topic)}" placeholder="home/status/power">
</div>
<div class="condition-field">
<label>${t('profiles.condition.mqtt.payload')}</label>
<label>${t('automations.condition.mqtt.payload')}</label>
<input type="text" class="condition-mqtt-payload" value="${escapeHtml(payload)}" placeholder="ON">
</div>
<div class="condition-field">
<label>${t('profiles.condition.mqtt.match_mode')}</label>
<label>${t('automations.condition.mqtt.match_mode')}</label>
<select class="condition-mqtt-match-mode">
<option value="exact" ${matchMode === 'exact' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.exact')}</option>
<option value="contains" ${matchMode === 'contains' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.contains')}</option>
<option value="regex" ${matchMode === 'regex' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.regex')}</option>
<option value="exact" ${matchMode === 'exact' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.exact')}</option>
<option value="contains" ${matchMode === 'contains' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.contains')}</option>
<option value="regex" ${matchMode === 'regex' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.regex')}</option>
</select>
</div>
</div>`;
@@ -483,22 +483,22 @@ function addProfileConditionRow(condition) {
container.innerHTML = `
<div class="condition-fields">
<div class="condition-field">
<label>${t('profiles.condition.application.match_type')}</label>
<label>${t('automations.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>
<option value="running" ${matchType === 'running' ? 'selected' : ''}>${t('automations.condition.application.match_type.running')}</option>
<option value="topmost" ${matchType === 'topmost' ? 'selected' : ''}>${t('automations.condition.application.match_type.topmost')}</option>
<option value="topmost_fullscreen" ${matchType === 'topmost_fullscreen' ? 'selected' : ''}>${t('automations.condition.application.match_type.topmost_fullscreen')}</option>
<option value="fullscreen" ${matchType === 'fullscreen' ? 'selected' : ''}>${t('automations.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>
<label>${t('automations.condition.application.apps')}</label>
<button type="button" class="btn-browse-apps" title="${t('automations.condition.application.browse')}">${t('automations.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">
<input type="text" class="process-picker-search" placeholder="${t('automations.condition.application.search')}" autocomplete="off">
<div class="process-picker-list"></div>
</div>
</div>
@@ -551,7 +551,7 @@ async function toggleProcessPicker(picker, row) {
function renderProcessPicker(picker, processes, existing) {
const listEl = picker.querySelector('.process-picker-list');
if (processes.length === 0) {
listEl.innerHTML = `<div class="process-picker-loading">${t('profiles.condition.application.no_processes')}</div>`;
listEl.innerHTML = `<div class="process-picker-loading">${t('automations.condition.application.no_processes')}</div>`;
return;
}
listEl.innerHTML = processes.map(p => {
@@ -562,7 +562,7 @@ function renderProcessPicker(picker, processes, existing) {
listEl.querySelectorAll('.process-picker-item:not(.added)').forEach(item => {
item.addEventListener('click', () => {
const proc = item.dataset.process;
const row = picker.closest('.profile-condition-row');
const row = picker.closest('.automation-condition-row');
const textarea = row.querySelector('.condition-apps');
const current = textarea.value.trim();
textarea.value = current ? current + '\n' + proc : proc;
@@ -579,8 +579,8 @@ function filterProcessPicker(picker) {
renderProcessPicker(picker, filtered, picker._existing || new Set());
}
function getProfileEditorConditions() {
const rows = document.querySelectorAll('#profile-conditions-list .profile-condition-row');
function getAutomationEditorConditions() {
const rows = document.querySelectorAll('#automation-conditions-list .automation-condition-row');
const conditions = [];
rows.forEach(row => {
const typeSelect = row.querySelector('.condition-type-select');
@@ -621,15 +621,15 @@ function getProfileEditorConditions() {
return conditions;
}
export async function saveProfileEditor() {
const idInput = document.getElementById('profile-editor-id');
const nameInput = document.getElementById('profile-editor-name');
const enabledInput = document.getElementById('profile-editor-enabled');
const logicSelect = document.getElementById('profile-editor-logic');
export async function saveAutomationEditor() {
const idInput = document.getElementById('automation-editor-id');
const nameInput = document.getElementById('automation-editor-name');
const enabledInput = document.getElementById('automation-editor-enabled');
const logicSelect = document.getElementById('automation-editor-logic');
const name = nameInput.value.trim();
if (!name) {
profileModal.showError(t('profiles.error.name_required'));
automationModal.showError(t('automations.error.name_required'));
return;
}
@@ -637,61 +637,61 @@ export async function saveProfileEditor() {
name,
enabled: enabledInput.checked,
condition_logic: logicSelect.value,
conditions: getProfileEditorConditions(),
scene_preset_id: document.getElementById('profile-scene-id').value || null,
deactivation_mode: document.getElementById('profile-deactivation-mode').value,
deactivation_scene_preset_id: document.getElementById('profile-fallback-scene-id').value || null,
conditions: getAutomationEditorConditions(),
scene_preset_id: document.getElementById('automation-scene-id').value || null,
deactivation_mode: document.getElementById('automation-deactivation-mode').value,
deactivation_scene_preset_id: document.getElementById('automation-fallback-scene-id').value || null,
};
const profileId = idInput.value;
const isEdit = !!profileId;
const automationId = idInput.value;
const isEdit = !!automationId;
try {
const url = isEdit ? `/profiles/${profileId}` : '/profiles';
const url = isEdit ? `/automations/${automationId}` : '/automations';
const resp = await fetchWithAuth(url, {
method: isEdit ? 'PUT' : 'POST',
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || 'Failed to save profile');
throw new Error(err.detail || 'Failed to save automation');
}
profileModal.forceClose();
showToast(isEdit ? t('profiles.updated') : t('profiles.created'), 'success');
loadProfiles();
automationModal.forceClose();
showToast(isEdit ? t('automations.updated') : t('automations.created'), 'success');
loadAutomations();
} catch (e) {
if (e.isAuth) return;
profileModal.showError(e.message);
automationModal.showError(e.message);
}
}
export async function toggleProfileEnabled(profileId, enable) {
export async function toggleAutomationEnabled(automationId, enable) {
try {
const action = enable ? 'enable' : 'disable';
const resp = await fetchWithAuth(`/profiles/${profileId}/${action}`, {
const resp = await fetchWithAuth(`/automations/${automationId}/${action}`, {
method: 'POST',
});
if (!resp.ok) throw new Error(`Failed to ${action} profile`);
loadProfiles();
if (!resp.ok) throw new Error(`Failed to ${action} automation`);
loadAutomations();
} catch (e) {
if (e.isAuth) return;
showToast(e.message, 'error');
}
}
export async function deleteProfile(profileId, profileName) {
const msg = t('profiles.delete.confirm').replace('{name}', profileName);
export async function deleteAutomation(automationId, automationName) {
const msg = t('automations.delete.confirm').replace('{name}', automationName);
const confirmed = await showConfirm(msg);
if (!confirmed) return;
try {
const resp = await fetchWithAuth(`/profiles/${profileId}`, {
const resp = await fetchWithAuth(`/automations/${automationId}`, {
method: 'DELETE',
});
if (!resp.ok) throw new Error('Failed to delete profile');
showToast(t('profiles.deleted'), 'success');
loadProfiles();
if (!resp.ok) throw new Error('Failed to delete automation');
showToast(t('automations.deleted'), 'success');
loadAutomations();
} catch (e) {
if (e.isAuth) return;
showToast(e.message, 'error');

View File

@@ -10,7 +10,7 @@ import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling }
import { startAutoRefresh, updateTabBadge } from './tabs.js';
import {
getTargetTypeIcon,
ICON_TARGET, ICON_PROFILE, ICON_CLOCK, ICON_WARNING, ICON_OK,
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
} from '../core/icons.js';
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
@@ -252,28 +252,28 @@ function _updateRunningMetrics(enrichedRunning) {
}
function _updateProfilesInPlace(profiles) {
for (const p of profiles) {
const card = document.querySelector(`[data-profile-id="${p.id}"]`);
function _updateAutomationsInPlace(automations) {
for (const a of automations) {
const card = document.querySelector(`[data-automation-id="${a.id}"]`);
if (!card) continue;
const badge = card.querySelector('.dashboard-badge-active, .dashboard-badge-stopped');
if (badge) {
if (!p.enabled) {
if (!a.enabled) {
badge.className = 'dashboard-badge-stopped';
badge.textContent = t('profiles.status.disabled');
} else if (p.is_active) {
badge.textContent = t('automations.status.disabled');
} else if (a.is_active) {
badge.className = 'dashboard-badge-active';
badge.textContent = t('profiles.status.active');
badge.textContent = t('automations.status.active');
} else {
badge.className = 'dashboard-badge-stopped';
badge.textContent = t('profiles.status.inactive');
badge.textContent = t('automations.status.inactive');
}
}
const btn = card.querySelector('.dashboard-target-actions .dashboard-action-btn');
if (btn) {
btn.className = `dashboard-action-btn ${p.enabled ? 'stop' : 'start'}`;
btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`);
btn.innerHTML = p.enabled ? ICON_STOP_PLAIN : ICON_START;
btn.className = `dashboard-action-btn ${a.enabled ? 'stop' : 'start'}`;
btn.setAttribute('onclick', `dashboardToggleAutomation('${a.id}', ${!a.enabled})`);
btn.innerHTML = a.enabled ? ICON_STOP_PLAIN : ICON_START;
}
}
}
@@ -368,9 +368,9 @@ export async function loadDashboard(forceFullRender = false) {
try {
// Fire all requests in a single batch to avoid sequential RTTs
const [targetsResp, profilesResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets] = await Promise.all([
const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets] = await Promise.all([
fetchWithAuth('/picture-targets'),
fetchWithAuth('/profiles').catch(() => null),
fetchWithAuth('/automations').catch(() => null),
fetchWithAuth('/devices').catch(() => null),
fetchWithAuth('/color-strip-sources').catch(() => null),
fetchWithAuth('/picture-targets/batch/states').catch(() => null),
@@ -380,8 +380,8 @@ export async function loadDashboard(forceFullRender = false) {
const targetsData = await targetsResp.json();
const targets = targetsData.targets || [];
const profilesData = profilesResp && profilesResp.ok ? await profilesResp.json() : { profiles: [] };
const profiles = profilesData.profiles || [];
const automationsData = automationsResp && automationsResp.ok ? await automationsResp.json() : { automations: [] };
const automations = automationsData.automations || [];
const devicesData = devicesResp && devicesResp.ok ? await devicesResp.json() : { devices: [] };
const devicesMap = {};
for (const d of (devicesData.devices || [])) { devicesMap[d.id] = d; }
@@ -392,12 +392,12 @@ export async function loadDashboard(forceFullRender = false) {
const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
// Build dynamic HTML (targets, profiles)
// Build dynamic HTML (targets, automations)
let dynamicHtml = '';
let runningIds = [];
let newAutoStartIds = '';
if (targets.length === 0 && profiles.length === 0 && scenePresets.length === 0) {
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0) {
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
} else {
const enriched = targets.map(target => ({
@@ -426,7 +426,7 @@ export async function loadDashboard(forceFullRender = false) {
}
if (structureUnchanged && forceFullRender) {
if (running.length > 0) _updateRunningMetrics(running);
_updateProfilesInPlace(profiles);
_updateAutomationsInPlace(automations);
_cacheUptimeElements();
_startUptimeTimer();
startPerfPolling();
@@ -451,8 +451,8 @@ export async function loadDashboard(forceFullRender = false) {
}
}
const statusBadge = isRunning
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
? `<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';
@@ -480,16 +480,16 @@ export async function loadDashboard(forceFullRender = false) {
</div>`;
}
if (profiles.length > 0) {
const activeProfiles = profiles.filter(p => p.is_active);
const inactiveProfiles = profiles.filter(p => !p.is_active);
updateTabBadge('profiles', activeProfiles.length);
if (automations.length > 0) {
const activeAutomations = automations.filter(a => a.is_active);
const inactiveAutomations = automations.filter(a => !a.is_active);
updateTabBadge('automations', activeAutomations.length);
const sceneMap = new Map(scenePresets.map(s => [s.id, s]));
const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p, sceneMap)).join('');
const automationItems = [...activeAutomations, ...inactiveAutomations].map(a => renderDashboardAutomation(a, sceneMap)).join('');
dynamicHtml += `<div class="dashboard-section">
${_sectionHeader('profiles', t('dashboard.section.profiles'), profiles.length)}
${_sectionContent('profiles', profileItems)}
${_sectionHeader('automations', t('dashboard.section.automations'), automations.length)}
${_sectionContent('automations', automationItems)}
</div>`;
}
@@ -664,56 +664,56 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
}
}
function renderDashboardProfile(profile, sceneMap = new Map()) {
const isActive = profile.is_active;
const isDisabled = !profile.enabled;
function renderDashboardAutomation(automation, sceneMap = new Map()) {
const isActive = automation.is_active;
const isDisabled = !automation.enabled;
let condSummary = '';
if (profile.conditions.length > 0) {
const parts = profile.conditions.map(c => {
if (automation.conditions.length > 0) {
const parts = automation.conditions.map(c => {
if (c.condition_type === 'application') {
const apps = (c.apps || []).join(', ');
const matchLabel = c.match_type === 'topmost' ? t('profiles.condition.application.match_type.topmost') : t('profiles.condition.application.match_type.running');
const matchLabel = c.match_type === 'topmost' ? t('automations.condition.application.match_type.topmost') : t('automations.condition.application.match_type.running');
return `${apps} (${matchLabel})`;
}
return c.condition_type;
});
const logic = profile.condition_logic === 'and' ? ' & ' : ' | ';
const logic = automation.condition_logic === 'and' ? ' & ' : ' | ';
condSummary = parts.join(logic);
}
const statusBadge = isDisabled
? `<span class="dashboard-badge-stopped">${t('profiles.status.disabled')}</span>`
? `<span class="dashboard-badge-stopped">${t('automations.status.disabled')}</span>`
: isActive
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
? `<span class="dashboard-badge-active">${t('automations.status.active')}</span>`
: `<span class="dashboard-badge-stopped">${t('automations.status.inactive')}</span>`;
// Scene info
const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null;
const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected');
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-profile dashboard-card-link" data-profile-id="${profile.id}" onclick="if(!event.target.closest('button')){navigateToCard('profiles',null,'profiles','data-profile-id','${profile.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}')}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_PROFILE}</span>
<span class="dashboard-target-icon">${ICON_AUTOMATION}</span>
<div>
<div class="dashboard-target-name">${escapeHtml(profile.name)}</div>
<div class="dashboard-target-name">${escapeHtml(automation.name)}</div>
${condSummary ? `<div class="dashboard-target-subtitle">${escapeHtml(condSummary)}</div>` : ''}
<div class="dashboard-target-subtitle">${ICON_SCENE} ${sceneName}</div>
</div>
${statusBadge}
</div>
<div class="dashboard-target-actions">
<button class="dashboard-action-btn ${profile.enabled ? 'stop' : 'start'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
${profile.enabled ? ICON_STOP_PLAIN : ICON_START}
<button class="dashboard-action-btn ${automation.enabled ? 'stop' : 'start'}" onclick="dashboardToggleAutomation('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
${automation.enabled ? ICON_STOP_PLAIN : ICON_START}
</button>
</div>
</div>`;
}
export async function dashboardToggleProfile(profileId, enable) {
export async function dashboardToggleAutomation(automationId, enable) {
try {
const endpoint = enable ? 'enable' : 'disable';
const response = await fetchWithAuth(`/profiles/${profileId}/${endpoint}`, {
const response = await fetchWithAuth(`/automations/${automationId}/${endpoint}`, {
method: 'POST',
});
if (response.ok) {
@@ -721,7 +721,7 @@ export async function dashboardToggleProfile(profileId, enable) {
}
} catch (error) {
if (error.isAuth) return;
showToast(t('dashboard.error.profile_toggle_failed'), 'error');
showToast(t('dashboard.error.automation_toggle_failed'), 'error');
}
}
@@ -817,7 +817,7 @@ function _debouncedDashboardReload(forceFullRender = false) {
}
document.addEventListener('server:state_change', () => _debouncedDashboardReload());
document.addEventListener('server:profile_state_changed', () => _debouncedDashboardReload(true));
document.addEventListener('server:automation_state_changed', () => _debouncedDashboardReload(true));
// Re-render dashboard when language changes
document.addEventListener('languageChanged', () => {

View File

@@ -85,13 +85,13 @@ export function collapseAllSceneSections() {
function _createSceneCard(preset) {
const targetCount = (preset.targets || []).length;
const deviceCount = (preset.devices || []).length;
const profileCount = (preset.profiles || []).length;
const automationCount = (preset.automations || []).length;
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
const meta = [
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
deviceCount > 0 ? `${ICON_SETTINGS} ${deviceCount} ${t('scenes.devices_count')}` : null,
profileCount > 0 ? `${profileCount} ${t('scenes.profiles_count')}` : null,
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
].filter(Boolean);
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
@@ -143,12 +143,12 @@ function _renderDashboardPresetCard(preset) {
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
const targetCount = (preset.targets || []).length;
const deviceCount = (preset.devices || []).length;
const profileCount = (preset.profiles || []).length;
const automationCount = (preset.automations || []).length;
const subtitle = [
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
deviceCount > 0 ? `${deviceCount} ${t('scenes.devices_count')}` : null,
profileCount > 0 ? `${profileCount} ${t('scenes.profiles_count')}` : null,
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
].filter(Boolean).join(' \u00b7 ');
return `<div class="dashboard-target dashboard-scene-preset" data-scene-id="${preset.id}" style="${borderStyle}">

View File

@@ -58,8 +58,8 @@ export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
if (typeof window.loadPictureSources === 'function') window.loadPictureSources();
} else if (name === 'targets') {
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} else if (name === 'profiles') {
if (typeof window.loadProfiles === 'function') window.loadProfiles();
} else if (name === 'automations') {
if (typeof window.loadAutomations === 'function') window.loadAutomations();
} else if (name === 'scenes') {
if (typeof window.loadScenes === 'function') window.loadScenes();
}

View File

@@ -24,7 +24,7 @@ const TOUR_KEY = 'tour_completed';
const gettingStartedSteps = [
{ selector: 'header .header-title', textKey: 'tour.welcome', position: 'bottom' },
{ selector: '#tab-btn-dashboard', textKey: 'tour.dashboard', position: 'bottom' },
{ selector: '#tab-btn-profiles', textKey: 'tour.profiles', position: 'bottom' },
{ selector: '#tab-btn-automations', textKey: 'tour.automations', position: 'bottom' },
{ selector: '#tab-btn-targets', textKey: 'tour.targets', position: 'bottom' },
{ selector: '#tab-btn-streams', textKey: 'tour.sources', position: 'bottom' },
{ selector: 'a.header-link[href="/docs"]', textKey: 'tour.api', position: 'bottom' },
@@ -39,7 +39,7 @@ const dashboardTutorialSteps = [
{ selector: '[data-dashboard-section="perf"]', textKey: 'tour.dash.perf', position: 'bottom' },
{ selector: '[data-dashboard-section="running"]', textKey: 'tour.dash.running', position: 'bottom' },
{ selector: '[data-dashboard-section="stopped"]', textKey: 'tour.dash.stopped', position: 'bottom' },
{ selector: '[data-dashboard-section="profiles"]', textKey: 'tour.dash.profiles', position: 'bottom' }
{ selector: '[data-dashboard-section="automations"]', textKey: 'tour.dash.automations', position: 'bottom' }
];
const targetsTutorialSteps = [
@@ -59,10 +59,10 @@ const sourcesTourSteps = [
{ selector: '[data-stream-tab="value"]', textKey: 'tour.src.value', position: 'bottom' }
];
const profilesTutorialSteps = [
{ selector: '[data-card-section="profiles"]', textKey: 'tour.prof.list', position: 'bottom' },
{ selector: '[data-cs-add="profiles"]', textKey: 'tour.prof.add', position: 'bottom' },
{ selector: '.card[data-profile-id]', textKey: 'tour.prof.card', position: 'bottom' }
const automationsTutorialSteps = [
{ selector: '[data-card-section="automations"]', textKey: 'tour.auto.list', position: 'bottom' },
{ selector: '[data-cs-add="automations"]', textKey: 'tour.auto.add', position: 'bottom' },
{ selector: '.card[data-automation-id]', textKey: 'tour.auto.card', position: 'bottom' }
];
const _fixedResolve = (step) => {
@@ -184,9 +184,9 @@ export function startSourcesTutorial() {
});
}
export function startProfilesTutorial() {
export function startAutomationsTutorial() {
startTutorial({
steps: profilesTutorialSteps,
steps: automationsTutorialSteps,
overlayId: 'getting-started-overlay',
mode: 'fixed',
container: null,

View File

@@ -220,10 +220,10 @@
"calibration.tip.skip_leds_start": "Skip LEDs at the start of the strip — skipped LEDs stay off",
"calibration.tip.skip_leds_end": "Skip LEDs at the end of the strip — skipped LEDs stay off",
"tour.welcome": "Welcome to LED Grab! This quick tour will show you around the interface. Use arrow keys or buttons to navigate.",
"tour.dashboard": "Dashboard — live overview of running targets, profiles, and device health at a glance.",
"tour.dashboard": "Dashboard — live overview of running targets, automations, and device health at a glance.",
"tour.targets": "Targets — add WLED devices, configure LED targets with capture settings and calibration.",
"tour.sources": "Sources — manage capture templates, picture sources, audio sources, and color strips.",
"tour.profiles": "Profiles — group targets and automate switching with time, audio, or value conditions.",
"tour.automations": "Automations — automate scene switching with time, audio, or value conditions.",
"tour.settings": "Settings — backup and restore configuration, manage auto-backups.",
"tour.api": "API Docs — interactive REST API documentation powered by Swagger.",
"tour.search": "Search — quickly find and navigate to any entity with Ctrl+K.",
@@ -234,7 +234,7 @@
"tour.dash.perf": "Performance — real-time FPS charts, latency metrics, and poll interval control.",
"tour.dash.running": "Running targets — live streaming metrics and quick stop control.",
"tour.dash.stopped": "Stopped targets — ready to start with one click.",
"tour.dash.profiles": "Profiles — active profile status and quick enable/disable toggle.",
"tour.dash.automations": "Automations — active automation status and quick enable/disable toggle.",
"tour.tgt.led_tab": "LED tab — standard LED strip targets with device and color strip configuration.",
"tour.tgt.devices": "Devices — your WLED controllers discovered on the network.",
"tour.tgt.css": "Color Strips — define how screen regions map to LED segments.",
@@ -245,10 +245,10 @@
"tour.src.static": "Static Image — test your setup with image files instead of live capture.",
"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 profile automation.",
"tour.prof.list": "Profiles — automate target control based on time, audio, or value conditions.",
"tour.prof.add": "Click + to create a new profile with targets and activation conditions.",
"tour.prof.card": "Each card shows profile status, conditions, and quick controls to edit or toggle.",
"tour.src.value": "Value — numeric data sources used as conditions in automations.",
"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.",
"calibration.tutorial.start": "Start tutorial",
"calibration.overlay_toggle": "Overlay",
"calibration.start_position": "Starting Position:",
@@ -531,7 +531,7 @@
"dashboard.device": "Device",
"dashboard.stop_all": "Stop All",
"dashboard.failed": "Failed to load dashboard",
"dashboard.section.profiles": "Profiles",
"dashboard.section.automations": "Automations",
"dashboard.section.scenes": "Scene Presets",
"dashboard.targets": "Targets",
"dashboard.section.performance": "System Performance",
@@ -541,83 +541,83 @@
"dashboard.perf.unavailable": "unavailable",
"dashboard.perf.color": "Chart color",
"dashboard.poll_interval": "Refresh interval",
"profiles.title": "Profiles",
"profiles.empty": "No profiles configured. Create one to automate target activation.",
"profiles.add": "Add Profile",
"profiles.edit": "Edit Profile",
"profiles.delete.confirm": "Delete profile \"{name}\"?",
"profiles.name": "Name:",
"profiles.name.hint": "A descriptive name for this profile",
"profiles.enabled": "Enabled:",
"profiles.enabled.hint": "Disabled profiles won't activate even when conditions are met",
"profiles.condition_logic": "Condition Logic:",
"profiles.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)",
"profiles.condition_logic.or": "Any condition (OR)",
"profiles.condition_logic.and": "All conditions (AND)",
"profiles.conditions": "Conditions:",
"profiles.conditions.hint": "Rules that determine when this profile activates",
"profiles.conditions.add": "Add Condition",
"profiles.conditions.empty": "No conditions — 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)",
"profiles.condition.application.browse": "Browse",
"profiles.condition.application.search": "Filter processes...",
"profiles.condition.application.no_processes": "No processes found",
"profiles.condition.application.match_type": "Match Type:",
"profiles.condition.application.match_type.hint": "How to detect the application",
"profiles.condition.application.match_type.running": "Running",
"profiles.condition.application.match_type.topmost": "Topmost (foreground)",
"profiles.condition.application.match_type.topmost_fullscreen": "Topmost + Fullscreen",
"profiles.condition.application.match_type.fullscreen": "Fullscreen",
"profiles.condition.time_of_day": "Time of Day",
"profiles.condition.time_of_day.start_time": "Start Time:",
"profiles.condition.time_of_day.end_time": "End Time:",
"profiles.condition.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:0006:00), set start time after end time.",
"profiles.condition.system_idle": "System Idle",
"profiles.condition.system_idle.idle_minutes": "Idle Timeout (minutes):",
"profiles.condition.system_idle.mode": "Trigger Mode:",
"profiles.condition.system_idle.when_idle": "When idle",
"profiles.condition.system_idle.when_active": "When active",
"profiles.condition.display_state": "Display State",
"profiles.condition.display_state.state": "Monitor State:",
"profiles.condition.display_state.on": "On",
"profiles.condition.display_state.off": "Off (sleeping)",
"profiles.condition.mqtt": "MQTT",
"profiles.condition.mqtt.topic": "Topic:",
"profiles.condition.mqtt.payload": "Payload:",
"profiles.condition.mqtt.match_mode": "Match Mode:",
"profiles.condition.mqtt.match_mode.exact": "Exact",
"profiles.condition.mqtt.match_mode.contains": "Contains",
"profiles.condition.mqtt.match_mode.regex": "Regex",
"profiles.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload",
"profiles.scene": "Scene:",
"profiles.scene.hint": "Scene preset to activate when conditions are met",
"profiles.scene.search_placeholder": "Search scenes...",
"profiles.scene.none_selected": "No scene",
"profiles.scene.none_available": "No scenes available",
"profiles.deactivation_mode": "Deactivation:",
"profiles.deactivation_mode.hint": "What happens when conditions stop matching",
"profiles.deactivation_mode.none": "None — keep current state",
"profiles.deactivation_mode.revert": "Revert to previous state",
"profiles.deactivation_mode.fallback_scene": "Activate fallback scene",
"profiles.deactivation_scene": "Fallback Scene:",
"profiles.deactivation_scene.hint": "Scene to activate when this profile deactivates",
"profiles.status.active": "Active",
"profiles.status.inactive": "Inactive",
"profiles.status.disabled": "Disabled",
"profiles.action.disable": "Disable",
"profiles.last_activated": "Last activated",
"profiles.logic.and": " AND ",
"profiles.logic.or": " OR ",
"profiles.logic.all": "ALL",
"profiles.logic.any": "ANY",
"profiles.updated": "Profile updated",
"profiles.created": "Profile created",
"profiles.deleted": "Profile deleted",
"profiles.error.name_required": "Name is required",
"automations.title": "Automations",
"automations.empty": "No automations configured. Create one to automate scene activation.",
"automations.add": "Add Automation",
"automations.edit": "Edit Automation",
"automations.delete.confirm": "Delete automation \"{name}\"?",
"automations.name": "Name:",
"automations.name.hint": "A descriptive name for this automation",
"automations.enabled": "Enabled:",
"automations.enabled.hint": "Disabled automations won't activate even when conditions are met",
"automations.condition_logic": "Condition Logic:",
"automations.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)",
"automations.condition_logic.or": "Any condition (OR)",
"automations.condition_logic.and": "All conditions (AND)",
"automations.conditions": "Conditions:",
"automations.conditions.hint": "Rules that determine when this automation activates",
"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.application": "Application",
"automations.condition.application.apps": "Applications:",
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
"automations.condition.application.browse": "Browse",
"automations.condition.application.search": "Filter processes...",
"automations.condition.application.no_processes": "No processes found",
"automations.condition.application.match_type": "Match Type:",
"automations.condition.application.match_type.hint": "How to detect the application",
"automations.condition.application.match_type.running": "Running",
"automations.condition.application.match_type.topmost": "Topmost (foreground)",
"automations.condition.application.match_type.topmost_fullscreen": "Topmost + Fullscreen",
"automations.condition.application.match_type.fullscreen": "Fullscreen",
"automations.condition.time_of_day": "Time of Day",
"automations.condition.time_of_day.start_time": "Start Time:",
"automations.condition.time_of_day.end_time": "End Time:",
"automations.condition.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:0006:00), set start time after end time.",
"automations.condition.system_idle": "System Idle",
"automations.condition.system_idle.idle_minutes": "Idle Timeout (minutes):",
"automations.condition.system_idle.mode": "Trigger Mode:",
"automations.condition.system_idle.when_idle": "When idle",
"automations.condition.system_idle.when_active": "When active",
"automations.condition.display_state": "Display State",
"automations.condition.display_state.state": "Monitor State:",
"automations.condition.display_state.on": "On",
"automations.condition.display_state.off": "Off (sleeping)",
"automations.condition.mqtt": "MQTT",
"automations.condition.mqtt.topic": "Topic:",
"automations.condition.mqtt.payload": "Payload:",
"automations.condition.mqtt.match_mode": "Match Mode:",
"automations.condition.mqtt.match_mode.exact": "Exact",
"automations.condition.mqtt.match_mode.contains": "Contains",
"automations.condition.mqtt.match_mode.regex": "Regex",
"automations.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload",
"automations.scene": "Scene:",
"automations.scene.hint": "Scene preset to activate when conditions are met",
"automations.scene.search_placeholder": "Search scenes...",
"automations.scene.none_selected": "No scene",
"automations.scene.none_available": "No scenes available",
"automations.deactivation_mode": "Deactivation:",
"automations.deactivation_mode.hint": "What happens when conditions stop matching",
"automations.deactivation_mode.none": "None — keep current state",
"automations.deactivation_mode.revert": "Revert to previous state",
"automations.deactivation_mode.fallback_scene": "Activate fallback scene",
"automations.deactivation_scene": "Fallback Scene:",
"automations.deactivation_scene.hint": "Scene to activate when this automation deactivates",
"automations.status.active": "Active",
"automations.status.inactive": "Inactive",
"automations.status.disabled": "Disabled",
"automations.action.disable": "Disable",
"automations.last_activated": "Last activated",
"automations.logic.and": " AND ",
"automations.logic.or": " OR ",
"automations.logic.all": "ALL",
"automations.logic.any": "ANY",
"automations.updated": "Automation updated",
"automations.created": "Automation created",
"automations.deleted": "Automation deleted",
"automations.error.name_required": "Name is required",
"scenes.title": "Scenes",
"scenes.add": "Capture Scene",
"scenes.edit": "Edit Scene",
@@ -633,7 +633,7 @@
"scenes.delete": "Delete scene",
"scenes.targets_count": "targets",
"scenes.devices_count": "devices",
"scenes.profiles_count": "profiles",
"scenes.automations_count": "automations",
"scenes.captured": "Scene captured",
"scenes.updated": "Scene updated",
"scenes.activated": "Scene activated",
@@ -1016,7 +1016,7 @@
"search.group.targets": "LED Targets",
"search.group.kc_targets": "Key Colors Targets",
"search.group.css": "Color Strip Sources",
"search.group.profiles": "Profiles",
"search.group.automations": "Automations",
"search.group.streams": "Picture Streams",
"search.group.capture_templates": "Capture Templates",
"search.group.pp_templates": "Post-Processing Templates",
@@ -1025,7 +1025,7 @@
"search.group.value": "Value Sources",
"search.group.scenes": "Scene Presets",
"settings.backup.label": "Backup Configuration",
"settings.backup.hint": "Download all configuration (devices, targets, streams, templates, profiles) as a single JSON file.",
"settings.backup.hint": "Download all configuration (devices, targets, streams, templates, automations) as a single JSON file.",
"settings.backup.button": "Download Backup",
"settings.backup.success": "Backup downloaded successfully",
"settings.backup.error": "Backup download failed",
@@ -1074,7 +1074,7 @@
"calibration.error.save_failed": "Failed to save calibration",
"calibration.error.led_count_mismatch": "Total LEDs must equal the device LED count",
"calibration.error.led_count_exceeded": "Calibrated LEDs exceed the total LED count",
"dashboard.error.profile_toggle_failed": "Failed to toggle profile",
"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",

View File

@@ -220,10 +220,10 @@
"calibration.tip.skip_leds_start": "Пропуск LED в начале ленты — пропущенные LED остаются выключенными",
"calibration.tip.skip_leds_end": "Пропуск LED в конце ленты — пропущенные LED остаются выключенными",
"tour.welcome": "Добро пожаловать в LED Grab! Этот краткий тур познакомит вас с интерфейсом. Используйте стрелки или кнопки для навигации.",
"tour.dashboard": "Дашборд — обзор запущенных целей, профилей и состояния устройств.",
"tour.dashboard": "Дашборд — обзор запущенных целей, автоматизаций и состояния устройств.",
"tour.targets": "Цели — добавляйте WLED-устройства, настраивайте LED-цели с захватом и калибровкой.",
"tour.sources": "Источники — управление шаблонами захвата, источниками изображений, звука и цветовых полос.",
"tour.profiles": "Профили — группируйте цели и автоматизируйте переключение по расписанию, звуку или значениям.",
"tour.automations": "Автоматизации — автоматизируйте переключение сцен по расписанию, звуку или значениям.",
"tour.settings": "Настройки — резервное копирование и восстановление конфигурации.",
"tour.api": "API Документация — интерактивная документация REST API на базе Swagger.",
"tour.search": "Поиск — быстрый поиск и переход к любому объекту по Ctrl+K.",
@@ -234,7 +234,7 @@
"tour.dash.perf": "Производительность — графики FPS в реальном времени, метрики задержки и интервал опроса.",
"tour.dash.running": "Запущенные цели — метрики стриминга и быстрая остановка.",
"tour.dash.stopped": "Остановленные цели — готовы к запуску одним нажатием.",
"tour.dash.profiles": "Профили — статус активных профилей и быстрое включение/выключение.",
"tour.dash.automations": "Автоматизации — статус активных автоматизаций и быстрое включение/выключение.",
"tour.tgt.led_tab": "LED — стандартные LED-цели с настройкой устройств и цветовых полос.",
"tour.tgt.devices": "Устройства — ваши WLED-контроллеры, найденные в сети.",
"tour.tgt.css": "Цветовые полосы — определите, как области экрана соответствуют сегментам LED.",
@@ -245,10 +245,10 @@
"tour.src.static": "Статичные изображения — тестируйте настройку с файлами изображений.",
"tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.",
"tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.",
"tour.src.value": "Значения — числовые источники данных для условий автоматизации профилей.",
"tour.prof.list": "Профили — автоматизируйте управление целями по времени, звуку или значениям.",
"tour.prof.add": "Нажмите + для создания нового профиля с целями и условиями активации.",
"tour.prof.card": "Каждая карточка показывает статус профиля, условия и кнопки управления.",
"tour.src.value": "Значения — числовые источники данных для условий автоматизаций.",
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
"calibration.tutorial.start": "Начать обучение",
"calibration.overlay_toggle": "Оверлей",
"calibration.start_position": "Начальная Позиция:",
@@ -531,7 +531,7 @@
"dashboard.device": "Устройство",
"dashboard.stop_all": "Остановить все",
"dashboard.failed": "Не удалось загрузить обзор",
"dashboard.section.profiles": "Профили",
"dashboard.section.automations": "Автоматизации",
"dashboard.section.scenes": "Пресеты сцен",
"dashboard.targets": "Цели",
"dashboard.section.performance": "Производительность системы",
@@ -541,83 +541,83 @@
"dashboard.perf.unavailable": "недоступно",
"dashboard.perf.color": "Цвет графика",
"dashboard.poll_interval": "Интервал обновления",
"profiles.title": "Профили",
"profiles.empty": "Профили не настроены. Создайте профиль для автоматизации целей.",
"profiles.add": "Добавить профиль",
"profiles.edit": "Редактировать профиль",
"profiles.delete.confirm": "Удалить профиль \"{name}\"?",
"profiles.name": "Название:",
"profiles.name.hint": "Описательное имя для профиля",
"profiles.enabled": "Включён:",
"profiles.enabled.hint": "Отключённые профили не активируются даже при выполнении условий",
"profiles.condition_logic": "Логика условий:",
"profiles.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)",
"profiles.condition_logic.or": "Любое условие (ИЛИ)",
"profiles.condition_logic.and": "Все условия (И)",
"profiles.conditions": "Условия:",
"profiles.conditions.hint": "Правила, определяющие когда профиль активируется",
"profiles.conditions.add": "Добавить условие",
"profiles.conditions.empty": "Нет условий — профиль всегда активен когда включён",
"profiles.condition.always": "Всегда",
"profiles.condition.always.hint": "Профиль активируется сразу при включении и остаётся активным. Используйте для автозапуска целей при старте сервера.",
"profiles.condition.application": "Приложение",
"profiles.condition.application.apps": "Приложения:",
"profiles.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
"profiles.condition.application.browse": "Обзор",
"profiles.condition.application.search": "Фильтр процессов...",
"profiles.condition.application.no_processes": "Процессы не найдены",
"profiles.condition.application.match_type": "Тип соответствия:",
"profiles.condition.application.match_type.hint": "Как определять наличие приложения",
"profiles.condition.application.match_type.running": "Запущено",
"profiles.condition.application.match_type.topmost": "На переднем плане",
"profiles.condition.application.match_type.topmost_fullscreen": "На переднем плане + Полный экран",
"profiles.condition.application.match_type.fullscreen": "Полный экран",
"profiles.condition.time_of_day": "Время суток",
"profiles.condition.time_of_day.start_time": "Время начала:",
"profiles.condition.time_of_day.end_time": "Время окончания:",
"profiles.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:0006:00) укажите время начала позже времени окончания.",
"profiles.condition.system_idle": "Бездействие системы",
"profiles.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):",
"profiles.condition.system_idle.mode": "Режим срабатывания:",
"profiles.condition.system_idle.when_idle": "При бездействии",
"profiles.condition.system_idle.when_active": "При активности",
"profiles.condition.display_state": "Состояние дисплея",
"profiles.condition.display_state.state": "Состояние монитора:",
"profiles.condition.display_state.on": "Включён",
"profiles.condition.display_state.off": "Выключен (спящий режим)",
"profiles.condition.mqtt": "MQTT",
"profiles.condition.mqtt.topic": "Топик:",
"profiles.condition.mqtt.payload": "Значение:",
"profiles.condition.mqtt.match_mode": "Режим сравнения:",
"profiles.condition.mqtt.match_mode.exact": "Точное совпадение",
"profiles.condition.mqtt.match_mode.contains": "Содержит",
"profiles.condition.mqtt.match_mode.regex": "Регулярное выражение",
"profiles.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику",
"profiles.scene": "Сцена:",
"profiles.scene.hint": "Пресет сцены для активации при выполнении условий",
"profiles.scene.search_placeholder": "Поиск сцен...",
"profiles.scene.none_selected": "Нет сцены",
"profiles.scene.none_available": "Нет доступных сцен",
"profiles.deactivation_mode": "Деактивация:",
"profiles.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться",
"profiles.deactivation_mode.none": "Ничего — оставить текущее состояние",
"profiles.deactivation_mode.revert": "Вернуть предыдущее состояние",
"profiles.deactivation_mode.fallback_scene": "Активировать резервную сцену",
"profiles.deactivation_scene": "Резервная сцена:",
"profiles.deactivation_scene.hint": "Сцена для активации при деактивации профиля",
"profiles.status.active": "Активен",
"profiles.status.inactive": "Неактивен",
"profiles.status.disabled": "Отключён",
"profiles.action.disable": "Отключить",
"profiles.last_activated": "Последняя активация",
"profiles.logic.and": " И ",
"profiles.logic.or": " ИЛИ ",
"profiles.logic.all": "ВСЕ",
"profiles.logic.any": "ЛЮБОЕ",
"profiles.updated": "Профиль обновлён",
"profiles.created": "Профиль создан",
"profiles.deleted": "Профиль удалён",
"profiles.error.name_required": "Введите название",
"automations.title": "Автоматизации",
"automations.empty": "Автоматизации не настроены. Создайте автоматизацию для автоматической активации сцен.",
"automations.add": "Добавить автоматизацию",
"automations.edit": "Редактировать автоматизацию",
"automations.delete.confirm": "Удалить автоматизацию \"{name}\"?",
"automations.name": "Название:",
"automations.name.hint": "Описательное имя для автоматизации",
"automations.enabled": "Включена:",
"automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий",
"automations.condition_logic": "Логика условий:",
"automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)",
"automations.condition_logic.or": "Любое условие (ИЛИ)",
"automations.condition_logic.and": "Все условия (И)",
"automations.conditions": "Условия:",
"automations.conditions.hint": "Правила, определяющие когда автоматизация активируется",
"automations.conditions.add": "Добавить условие",
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена",
"automations.condition.always": "Всегда",
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной. Используйте для автозапуска сцен при старте сервера.",
"automations.condition.application": "Приложение",
"automations.condition.application.apps": "Приложения:",
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
"automations.condition.application.browse": "Обзор",
"automations.condition.application.search": "Фильтр процессов...",
"automations.condition.application.no_processes": "Процессы не найдены",
"automations.condition.application.match_type": "Тип соответствия:",
"automations.condition.application.match_type.hint": "Как определять наличие приложения",
"automations.condition.application.match_type.running": "Запущено",
"automations.condition.application.match_type.topmost": "На переднем плане",
"automations.condition.application.match_type.topmost_fullscreen": "На переднем плане + Полный экран",
"automations.condition.application.match_type.fullscreen": "Полный экран",
"automations.condition.time_of_day": "Время суток",
"automations.condition.time_of_day.start_time": "Время начала:",
"automations.condition.time_of_day.end_time": "Время окончания:",
"automations.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:0006:00) укажите время начала позже времени окончания.",
"automations.condition.system_idle": "Бездействие системы",
"automations.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):",
"automations.condition.system_idle.mode": "Режим срабатывания:",
"automations.condition.system_idle.when_idle": "При бездействии",
"automations.condition.system_idle.when_active": "При активности",
"automations.condition.display_state": "Состояние дисплея",
"automations.condition.display_state.state": "Состояние монитора:",
"automations.condition.display_state.on": "Включён",
"automations.condition.display_state.off": "Выключен (спящий режим)",
"automations.condition.mqtt": "MQTT",
"automations.condition.mqtt.topic": "Топик:",
"automations.condition.mqtt.payload": "Значение:",
"automations.condition.mqtt.match_mode": "Режим сравнения:",
"automations.condition.mqtt.match_mode.exact": "Точное совпадение",
"automations.condition.mqtt.match_mode.contains": "Содержит",
"automations.condition.mqtt.match_mode.regex": "Регулярное выражение",
"automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику",
"automations.scene": "Сцена:",
"automations.scene.hint": "Пресет сцены для активации при выполнении условий",
"automations.scene.search_placeholder": "Поиск сцен...",
"automations.scene.none_selected": "Нет сцены",
"automations.scene.none_available": "Нет доступных сцен",
"automations.deactivation_mode": "Деактивация:",
"automations.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться",
"automations.deactivation_mode.none": "Ничего — оставить текущее состояние",
"automations.deactivation_mode.revert": "Вернуть предыдущее состояние",
"automations.deactivation_mode.fallback_scene": "Активировать резервную сцену",
"automations.deactivation_scene": "Резервная сцена:",
"automations.deactivation_scene.hint": "Сцена для активации при деактивации автоматизации",
"automations.status.active": "Активна",
"automations.status.inactive": "Неактивна",
"automations.status.disabled": "Отключена",
"automations.action.disable": "Отключить",
"automations.last_activated": "Последняя активация",
"automations.logic.and": " И ",
"automations.logic.or": " ИЛИ ",
"automations.logic.all": "ВСЕ",
"automations.logic.any": "ЛЮБОЕ",
"automations.updated": "Автоматизация обновлена",
"automations.created": "Автоматизация создана",
"automations.deleted": "Автоматизация удалена",
"automations.error.name_required": "Введите название",
"scenes.title": "Сцены",
"scenes.add": "Захватить сцену",
"scenes.edit": "Редактировать сцену",
@@ -633,7 +633,7 @@
"scenes.delete": "Удалить сцену",
"scenes.targets_count": "целей",
"scenes.devices_count": "устройств",
"scenes.profiles_count": "профилей",
"scenes.automations_count": "автоматизаций",
"scenes.captured": "Сцена захвачена",
"scenes.updated": "Сцена обновлена",
"scenes.activated": "Сцена активирована",
@@ -1016,7 +1016,7 @@
"search.group.targets": "LED-цели",
"search.group.kc_targets": "Цели Key Colors",
"search.group.css": "Источники цветных лент",
"search.group.profiles": "Профили",
"search.group.automations": "Автоматизации",
"search.group.streams": "Потоки изображений",
"search.group.capture_templates": "Шаблоны захвата",
"search.group.pp_templates": "Шаблоны постобработки",
@@ -1025,7 +1025,7 @@
"search.group.value": "Источники значений",
"search.group.scenes": "Пресеты сцен",
"settings.backup.label": "Резервное копирование",
"settings.backup.hint": "Скачать всю конфигурацию (устройства, цели, потоки, шаблоны, профили) в виде одного JSON-файла.",
"settings.backup.hint": "Скачать всю конфигурацию (устройства, цели, потоки, шаблоны, автоматизации) в виде одного JSON-файла.",
"settings.backup.button": "Скачать резервную копию",
"settings.backup.success": "Резервная копия скачана",
"settings.backup.error": "Ошибка скачивания резервной копии",
@@ -1074,7 +1074,7 @@
"calibration.error.save_failed": "Не удалось сохранить калибровку",
"calibration.error.led_count_mismatch": "Общее количество LED должно совпадать с количеством LED устройства",
"calibration.error.led_count_exceeded": "Калиброванных LED больше, чем общее количество LED",
"dashboard.error.profile_toggle_failed": "Не удалось переключить профиль",
"dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию",
"dashboard.error.start_failed": "Не удалось запустить обработку",
"dashboard.error.stop_failed": "Не удалось остановить обработку",
"dashboard.error.autostart_toggle_failed": "Не удалось переключить автозапуск",

View File

@@ -220,10 +220,10 @@
"calibration.tip.skip_leds_start": "跳过灯带起始端的 LED — 被跳过的 LED 保持关闭",
"calibration.tip.skip_leds_end": "跳过灯带末尾端的 LED — 被跳过的 LED 保持关闭",
"tour.welcome": "欢迎使用 LED Grab快速导览将带您了解界面。使用方向键或按钮进行导航。",
"tour.dashboard": "仪表盘 — 实时查看运行中的目标、配置文件和设备状态。",
"tour.dashboard": "仪表盘 — 实时查看运行中的目标、自动化和设备状态。",
"tour.targets": "目标 — 添加 WLED 设备,配置 LED 目标的捕获设置和校准。",
"tour.sources": "来源 — 管理捕获模板、图片来源、音频来源和色带。",
"tour.profiles": "配置文件 — 将目标分组,并通过时间、音频或数值条件自动切换。",
"tour.automations": "自动化 — 通过时间、音频或数值条件自动切换场景。",
"tour.settings": "设置 — 备份和恢复配置,管理自动备份。",
"tour.api": "API 文档 — 基于 Swagger 的交互式 REST API 文档。",
"tour.search": "搜索 — 使用 Ctrl+K 快速查找并导航到任意实体。",
@@ -234,7 +234,7 @@
"tour.dash.perf": "性能 — 实时 FPS 图表、延迟指标和轮询间隔控制。",
"tour.dash.running": "运行中的目标 — 实时流媒体指标和快速停止控制。",
"tour.dash.stopped": "已停止的目标 — 一键启动。",
"tour.dash.profiles": "配置文件 — 活动配置文件状态和快速启用/禁用切换。",
"tour.dash.automations": "自动化 — 活动自动化状态和快速启用/禁用切换。",
"tour.tgt.led_tab": "LED 标签 — 标准 LED 灯带目标,包含设备和色带配置。",
"tour.tgt.devices": "设备 — 在网络中发现的 WLED 控制器。",
"tour.tgt.css": "色带 — 定义屏幕区域如何映射到 LED 段。",
@@ -245,10 +245,10 @@
"tour.src.static": "静态图片 — 使用图片文件测试您的设置。",
"tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。",
"tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。",
"tour.src.value": "数值 — 用于配置文件自动化条件的数字数据源。",
"tour.prof.list": "配置文件 — 基于时间、音频或数值条件自动控制目标。",
"tour.prof.add": "点击 + 创建包含目标和激活条件的新配置文件。",
"tour.prof.card": "每张卡片显示配置文件状态、条件和快速编辑/切换控制。",
"tour.src.value": "数值 — 用于自动化条件的数字数据源。",
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
"calibration.tutorial.start": "开始教程",
"calibration.overlay_toggle": "叠加层",
"calibration.start_position": "起始位置:",
@@ -531,7 +531,7 @@
"dashboard.device": "设备",
"dashboard.stop_all": "全部停止",
"dashboard.failed": "加载仪表盘失败",
"dashboard.section.profiles": "配置文件",
"dashboard.section.automations": "自动化",
"dashboard.section.scenes": "场景预设",
"dashboard.targets": "目标",
"dashboard.section.performance": "系统性能",
@@ -541,83 +541,83 @@
"dashboard.perf.unavailable": "不可用",
"dashboard.perf.color": "图表颜色",
"dashboard.poll_interval": "刷新间隔",
"profiles.title": "配置文件",
"profiles.empty": "尚未配置配置文件。创建一个以自动化目标激活。",
"profiles.add": "添加配置文件",
"profiles.edit": "编辑配置文件",
"profiles.delete.confirm": "删除配置文件 \"{name}\"",
"profiles.name": "名称:",
"profiles.name.hint": "此配置文件的描述性名称",
"profiles.enabled": "启用:",
"profiles.enabled.hint": "禁用的配置文件即使满足条件也不会激活",
"profiles.condition_logic": "条件逻辑:",
"profiles.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)",
"profiles.condition_logic.or": "任一条件(或)",
"profiles.condition_logic.and": "全部条件(与)",
"profiles.conditions": "条件:",
"profiles.conditions.hint": "决定此配置文件何时激活的规则",
"profiles.conditions.add": "添加条件",
"profiles.conditions.empty": "无条件 — 启用后配置文件始终处于活动状态",
"profiles.condition.always": "始终",
"profiles.condition.always.hint": "配置文件启用后立即激活并保持活动。用于服务器启动时自动启动目标。",
"profiles.condition.application": "应用程序",
"profiles.condition.application.apps": "应用程序:",
"profiles.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe",
"profiles.condition.application.browse": "浏览",
"profiles.condition.application.search": "筛选进程...",
"profiles.condition.application.no_processes": "未找到进程",
"profiles.condition.application.match_type": "匹配类型:",
"profiles.condition.application.match_type.hint": "如何检测应用程序",
"profiles.condition.application.match_type.running": "运行中",
"profiles.condition.application.match_type.topmost": "最前(前台)",
"profiles.condition.application.match_type.topmost_fullscreen": "最前 + 全屏",
"profiles.condition.application.match_type.fullscreen": "全屏",
"profiles.condition.time_of_day": "时段",
"profiles.condition.time_of_day.start_time": "开始时间:",
"profiles.condition.time_of_day.end_time": "结束时间:",
"profiles.condition.time_of_day.overnight_hint": "跨夜时段(如 22:0006:00请将开始时间设为晚于结束时间。",
"profiles.condition.system_idle": "系统空闲",
"profiles.condition.system_idle.idle_minutes": "空闲超时(分钟):",
"profiles.condition.system_idle.mode": "触发模式:",
"profiles.condition.system_idle.when_idle": "空闲时",
"profiles.condition.system_idle.when_active": "活跃时",
"profiles.condition.display_state": "显示器状态",
"profiles.condition.display_state.state": "显示器状态:",
"profiles.condition.display_state.on": "开启",
"profiles.condition.display_state.off": "关闭(休眠)",
"profiles.condition.mqtt": "MQTT",
"profiles.condition.mqtt.topic": "主题:",
"profiles.condition.mqtt.payload": "消息内容:",
"profiles.condition.mqtt.match_mode": "匹配模式:",
"profiles.condition.mqtt.match_mode.exact": "精确匹配",
"profiles.condition.mqtt.match_mode.contains": "包含",
"profiles.condition.mqtt.match_mode.regex": "正则表达式",
"profiles.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活",
"profiles.scene": "场景:",
"profiles.scene.hint": "条件满足时激活的场景预设",
"profiles.scene.search_placeholder": "搜索场景...",
"profiles.scene.none_selected": "无场景",
"profiles.scene.none_available": "没有可用的场景",
"profiles.deactivation_mode": "停用方式:",
"profiles.deactivation_mode.hint": "条件不再满足时的行为",
"profiles.deactivation_mode.none": "无 — 保持当前状态",
"profiles.deactivation_mode.revert": "恢复到之前的状态",
"profiles.deactivation_mode.fallback_scene": "激活备用场景",
"profiles.deactivation_scene": "备用场景:",
"profiles.deactivation_scene.hint": "配置文件停用时激活的场景",
"profiles.status.active": "活动",
"profiles.status.inactive": "非活动",
"profiles.status.disabled": "已禁用",
"profiles.action.disable": "禁用",
"profiles.last_activated": "上次激活",
"profiles.logic.and": " 与 ",
"profiles.logic.or": " 或 ",
"profiles.logic.all": "全部",
"profiles.logic.any": "任一",
"profiles.updated": "配置文件已更新",
"profiles.created": "配置文件已创建",
"profiles.deleted": "配置文件已删除",
"profiles.error.name_required": "名称为必填项",
"automations.title": "自动化",
"automations.empty": "尚未配置自动化。创建一个以自动激活场景。",
"automations.add": "添加自动化",
"automations.edit": "编辑自动化",
"automations.delete.confirm": "删除自动化 \"{name}\"",
"automations.name": "名称:",
"automations.name.hint": "此自动化的描述性名称",
"automations.enabled": "启用:",
"automations.enabled.hint": "禁用的自动化即使满足条件也不会激活",
"automations.condition_logic": "条件逻辑:",
"automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)",
"automations.condition_logic.or": "任一条件(或)",
"automations.condition_logic.and": "全部条件(与)",
"automations.conditions": "条件:",
"automations.conditions.hint": "决定此自动化何时激活的规则",
"automations.conditions.add": "添加条件",
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态",
"automations.condition.always": "始终",
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。用于服务器启动时自动激活场景。",
"automations.condition.application": "应用程序",
"automations.condition.application.apps": "应用程序:",
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe",
"automations.condition.application.browse": "浏览",
"automations.condition.application.search": "筛选进程...",
"automations.condition.application.no_processes": "未找到进程",
"automations.condition.application.match_type": "匹配类型:",
"automations.condition.application.match_type.hint": "如何检测应用程序",
"automations.condition.application.match_type.running": "运行中",
"automations.condition.application.match_type.topmost": "最前(前台)",
"automations.condition.application.match_type.topmost_fullscreen": "最前 + 全屏",
"automations.condition.application.match_type.fullscreen": "全屏",
"automations.condition.time_of_day": "时段",
"automations.condition.time_of_day.start_time": "开始时间:",
"automations.condition.time_of_day.end_time": "结束时间:",
"automations.condition.time_of_day.overnight_hint": "跨夜时段(如 22:0006:00请将开始时间设为晚于结束时间。",
"automations.condition.system_idle": "系统空闲",
"automations.condition.system_idle.idle_minutes": "空闲超时(分钟):",
"automations.condition.system_idle.mode": "触发模式:",
"automations.condition.system_idle.when_idle": "空闲时",
"automations.condition.system_idle.when_active": "活跃时",
"automations.condition.display_state": "显示器状态",
"automations.condition.display_state.state": "显示器状态:",
"automations.condition.display_state.on": "开启",
"automations.condition.display_state.off": "关闭(休眠)",
"automations.condition.mqtt": "MQTT",
"automations.condition.mqtt.topic": "主题:",
"automations.condition.mqtt.payload": "消息内容:",
"automations.condition.mqtt.match_mode": "匹配模式:",
"automations.condition.mqtt.match_mode.exact": "精确匹配",
"automations.condition.mqtt.match_mode.contains": "包含",
"automations.condition.mqtt.match_mode.regex": "正则表达式",
"automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活",
"automations.scene": "场景:",
"automations.scene.hint": "条件满足时激活的场景预设",
"automations.scene.search_placeholder": "搜索场景...",
"automations.scene.none_selected": "无场景",
"automations.scene.none_available": "没有可用的场景",
"automations.deactivation_mode": "停用方式:",
"automations.deactivation_mode.hint": "条件不再满足时的行为",
"automations.deactivation_mode.none": "无 — 保持当前状态",
"automations.deactivation_mode.revert": "恢复到之前的状态",
"automations.deactivation_mode.fallback_scene": "激活备用场景",
"automations.deactivation_scene": "备用场景:",
"automations.deactivation_scene.hint": "自动化停用时激活的场景",
"automations.status.active": "活动",
"automations.status.inactive": "非活动",
"automations.status.disabled": "已禁用",
"automations.action.disable": "禁用",
"automations.last_activated": "上次激活",
"automations.logic.and": " 与 ",
"automations.logic.or": " 或 ",
"automations.logic.all": "全部",
"automations.logic.any": "任一",
"automations.updated": "自动化已更新",
"automations.created": "自动化已创建",
"automations.deleted": "自动化已删除",
"automations.error.name_required": "名称为必填项",
"scenes.title": "场景",
"scenes.add": "捕获场景",
"scenes.edit": "编辑场景",
@@ -633,7 +633,7 @@
"scenes.delete": "删除场景",
"scenes.targets_count": "目标",
"scenes.devices_count": "设备",
"scenes.profiles_count": "配置",
"scenes.automations_count": "自动化",
"scenes.captured": "场景已捕获",
"scenes.updated": "场景已更新",
"scenes.activated": "场景已激活",
@@ -1016,7 +1016,7 @@
"search.group.targets": "LED 目标",
"search.group.kc_targets": "关键颜色目标",
"search.group.css": "色带源",
"search.group.profiles": "配置文件",
"search.group.automations": "自动化",
"search.group.streams": "图片流",
"search.group.capture_templates": "采集模板",
"search.group.pp_templates": "后处理模板",
@@ -1025,7 +1025,7 @@
"search.group.value": "值源",
"search.group.scenes": "场景预设",
"settings.backup.label": "备份配置",
"settings.backup.hint": "将所有配置(设备、目标、流、模板、配置文件)下载为单个 JSON 文件。",
"settings.backup.hint": "将所有配置(设备、目标、流、模板、自动化)下载为单个 JSON 文件。",
"settings.backup.button": "下载备份",
"settings.backup.success": "备份下载成功",
"settings.backup.error": "备份下载失败",
@@ -1074,7 +1074,7 @@
"calibration.error.save_failed": "保存校准失败",
"calibration.error.led_count_mismatch": "LED总数必须等于设备LED数量",
"calibration.error.led_count_exceeded": "校准的LED超过了LED总数",
"dashboard.error.profile_toggle_failed": "切换配置文件失败",
"dashboard.error.automation_toggle_failed": "切换自动化失败",
"dashboard.error.start_failed": "启动处理失败",
"dashboard.error.stop_failed": "停止处理失败",
"dashboard.error.autostart_toggle_failed": "切换自动启动失败",