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

@@ -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', () => {