Replace auto-start with startup automation, add card colors to dashboard

- Add `startup` automation condition type that activates on server boot,
  replacing the per-target `auto_start` flag
- Remove `auto_start` field from targets, scene snapshots, and all API layers
- Remove auto-start UI section and star buttons from dashboard and target cards
- Remove `color` field from scene presets (backend, API, modal, frontend)
- Add card color support to scene preset cards (color picker + border style)
- Show localStorage-backed card colors on all dashboard cards (targets,
  automations, sync clocks, scene presets)
- Fix card color picker updating wrong card when duplicate data attributes
  exist by using closest() from picker wrapper instead of global querySelector
- Add sync clocks step to Sources tab tutorial
- Bump SW cache v9 → v10

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 01:09:27 +03:00
parent f08117eb7b
commit fddbd771f2
28 changed files with 78 additions and 211 deletions

View File

@@ -36,7 +36,7 @@ import {
} from './features/devices.js';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.js';
@@ -100,7 +100,7 @@ import {
startTargetProcessing, stopTargetProcessing,
stopAllLedTargets, stopAllKCTargets,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
cloneTarget, toggleLedPreview,
expandAllTargetSections, collapseAllTargetSections,
disconnectAllLedPreviewWS,
} from './features/targets.js';
@@ -211,7 +211,6 @@ Object.assign(window, {
dashboardToggleAutomation,
dashboardStartTarget,
dashboardStopTarget,
dashboardToggleAutoStart,
dashboardStopAll,
dashboardPauseClock,
dashboardResumeClock,
@@ -356,7 +355,6 @@ Object.assign(window, {
deleteTarget,
cloneTarget,
toggleLedPreview,
toggleTargetAutoStart,
disconnectAllLedPreviewWS,
// color-strip sources

View File

@@ -59,7 +59,10 @@ export function cardColorButton(entityId, cardAttr) {
registerColorPicker(pickerId, (hex) => {
setCardColor(entityId, hex);
const card = document.querySelector(`[${cardAttr}="${entityId}"]`);
// Find the card that contains this picker (not a global querySelector
// which could match a dashboard compact card first)
const wrapper = document.getElementById(`cp-wrap-${pickerId}`);
const card = wrapper?.closest(`[${cardAttr}]`);
if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
});

View File

@@ -113,6 +113,9 @@ function createAutomationCard(automation, sceneMap = new Map()) {
if (c.condition_type === 'always') {
return `<span class="stream-card-prop">${ICON_OK} ${t('automations.condition.always')}</span>`;
}
if (c.condition_type === 'startup') {
return `<span class="stream-card-prop">${ICON_START} ${t('automations.condition.startup')}</span>`;
}
if (c.condition_type === 'application') {
const apps = (c.apps || []).join(', ');
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
@@ -384,6 +387,7 @@ function addAutomationConditionRow(condition) {
<div class="condition-header">
<select class="condition-type-select">
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('automations.condition.always')}</option>
<option value="startup" ${condType === 'startup' ? 'selected' : ''}>${t('automations.condition.startup')}</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>
@@ -404,6 +408,10 @@ function addAutomationConditionRow(condition) {
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.always.hint')}</small>`;
return;
}
if (type === 'startup') {
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.startup.hint')}</small>`;
return;
}
if (type === 'time_of_day') {
const startTime = data.start_time || '00:00';
const endTime = data.end_time || '23:59';
@@ -612,6 +620,8 @@ function getAutomationEditorConditions() {
const condType = typeSelect ? typeSelect.value : 'application';
if (condType === 'always') {
conditions.push({ condition_type: 'always' });
} else if (condType === 'startup') {
conditions.push({ condition_type: 'startup' });
} else if (condType === 'time_of_day') {
conditions.push({
condition_type: 'time_of_day',

View File

@@ -10,9 +10,10 @@ import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling }
import { startAutoRefresh, updateTabBadge } from './tabs.js';
import {
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_HELP, ICON_SCENE,
} from '../core/icons.js';
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
import { cardColorStyle } from '../core/card-colors.js';
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
const MAX_FPS_SAMPLES = 120;
@@ -21,7 +22,6 @@ let _fpsHistory = {}; // { targetId: number[] } — fps_actual
let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
let _fpsCharts = {}; // { targetId: Chart }
let _lastRunningIds = []; // sorted target IDs from previous render
let _lastAutoStartIds = ''; // comma-joined sorted auto-start IDs
let _lastSyncClockIds = ''; // comma-joined sorted sync clock IDs
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
let _uptimeTimer = null;
@@ -308,7 +308,8 @@ function renderDashboardSyncClock(clock) {
clock.description ? escapeHtml(clock.description) : '',
].filter(Boolean).join(' · ');
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}">
const scStyle = cardColorStyle(clock.id);
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}"${scStyle ? ` style="${scStyle}"` : ''}>
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_CLOCK}</span>
<div>
@@ -447,8 +448,6 @@ export async function loadDashboard(forceFullRender = false) {
// Build dynamic HTML (targets, automations)
let dynamicHtml = '';
let runningIds = [];
let newAutoStartIds = '';
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
} else {
@@ -465,10 +464,9 @@ export async function loadDashboard(forceFullRender = false) {
// Check if we can do an in-place metrics update (same targets, not first load)
const newRunningIds = running.map(t => t.id).sort().join(',');
const prevRunningIds = [..._lastRunningIds].sort().join(',');
newAutoStartIds = enriched.filter(t => t.auto_start).map(t => t.id).sort().join(',');
const newSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newAutoStartIds === _lastAutoStartIds && newSyncClockIds === _lastSyncClockIds;
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newSyncClockIds === _lastSyncClockIds;
if (structureUnchanged && !forceFullRender && running.length > 0) {
_updateRunningMetrics(running);
_updateSyncClocksInPlace(syncClocks);
@@ -489,52 +487,6 @@ export async function loadDashboard(forceFullRender = false) {
return;
}
const autoStartTargets = enriched.filter(t => t.auto_start);
if (autoStartTargets.length > 0) {
const autoStartCards = autoStartTargets.map(target => {
const isRunning = !!(target.state && target.state.processing);
const isLed = target.target_type !== 'key_colors';
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
const subtitleParts = [typeLabel];
if (isLed) {
const device = target.device_id ? devicesMap[target.device_id] : null;
if (device) subtitleParts.push((device.device_type || '').toUpperCase());
const cssId = target.color_strip_source_id || '';
if (cssId) {
const css = cssSourceMap[cssId];
if (css) subtitleParts.push(t(`color_strip.type.${css.source_type}`) || css.source_type);
}
}
const statusBadge = isRunning
? `<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';
const asNavAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-target-id="${target.id}" onclick="if(!event.target.closest('button')){navigateToCard('targets','${asNavSub}','${asNavSec}','${asNavAttr}','${target.id}')}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
<div>
<div class="dashboard-target-name">${escapeHtml(target.name)} ${statusBadge}</div>
${subtitle}
</div>
</div>
<div class="dashboard-target-actions">
<button class="dashboard-action-btn ${isRunning ? 'stop' : 'start'}" onclick="${isRunning ? `dashboardStopTarget('${target.id}')` : `dashboardStartTarget('${target.id}')`}" title="${isRunning ? t('device.stop') : t('device.start')}">
${isRunning ? ICON_STOP_PLAIN : ICON_START}
</button>
</div>
</div>`;
}).join('');
const autoStartItems = `<div class="dashboard-autostart-grid">${autoStartCards}</div>`;
dynamicHtml += `<div class="dashboard-section">
${_sectionHeader('autostart', t('autostart.title'), autoStartTargets.length)}
${_sectionContent('autostart', autoStartItems)}
</div>`;
}
if (automations.length > 0) {
const activeAutomations = automations.filter(a => a.is_active);
const inactiveAutomations = automations.filter(a => !a.is_active);
@@ -617,7 +569,6 @@ export async function loadDashboard(forceFullRender = false) {
}
}
_lastRunningIds = runningIds;
_lastAutoStartIds = newAutoStartIds;
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
_cacheUptimeElements();
await _initFpsCharts(runningIds);
@@ -683,7 +634,8 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
healthDot = `<span class="health-dot ${cls}"></span>`;
}
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}">
const cStyle = cardColorStyle(target.id);
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}"${cStyle ? ` style="${cStyle}"` : ''}>
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${icon}</span>
<div>
@@ -708,12 +660,12 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
</div>
</div>
<div class="dashboard-target-actions">
<button class="dashboard-autostart-btn${target.auto_start ? ' active' : ''}" onclick="dashboardToggleAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">&#x2605;</button>
<button class="dashboard-action-btn stop" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">${ICON_STOP_PLAIN}</button>
</div>
</div>`;
} else {
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}">
const cStyle2 = cardColorStyle(target.id);
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}"${cStyle2 ? ` style="${cStyle2}"` : ''}>
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${icon}</span>
<div>
@@ -723,7 +675,6 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
</div>
<div class="dashboard-target-metrics"></div>
<div class="dashboard-target-actions">
<button class="dashboard-autostart-btn${target.auto_start ? ' active' : ''}" onclick="dashboardToggleAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">&#x2605;</button>
<button class="dashboard-action-btn start" onclick="dashboardStartTarget('${target.id}')" title="${t('device.button.start')}">${ICON_START}</button>
</div>
</div>`;
@@ -742,6 +693,7 @@ function renderDashboardAutomation(automation, sceneMap = new Map()) {
const matchLabel = c.match_type === 'topmost' ? t('automations.condition.application.match_type.topmost') : t('automations.condition.application.match_type.running');
return `${apps} (${matchLabel})`;
}
if (c.condition_type === 'startup') return t('automations.condition.startup');
return c.condition_type;
});
const logic = automation.condition_logic === 'and' ? ' & ' : ' | ';
@@ -758,7 +710,8 @@ function renderDashboardAutomation(automation, sceneMap = new Map()) {
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-automation dashboard-card-link" data-automation-id="${automation.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'automations','data-automation-id','${automation.id}')}">
const aStyle = cardColorStyle(automation.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}')}"${aStyle ? ` style="${aStyle}"` : ''}>
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_AUTOMATION}</span>
<div>
@@ -827,25 +780,6 @@ export async function dashboardStopTarget(targetId) {
}
}
export async function dashboardToggleAutoStart(targetId, enable) {
try {
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
method: 'PUT',
body: JSON.stringify({ auto_start: enable }),
});
if (response.ok) {
showToast(t(enable ? 'autostart.toggle.enabled' : 'autostart.toggle.disabled'), 'success');
loadDashboard();
} else {
const error = await response.json();
showToast(error.detail || t('dashboard.error.autostart_toggle_failed'), 'error');
}
} catch (error) {
if (error.isAuth) return;
showToast(t('dashboard.error.autostart_toggle_failed'), 'error');
}
}
export async function dashboardStopAll() {
try {
const [targetsResp, statesResp] = await Promise.all([

View File

@@ -122,7 +122,6 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
return wrapCard({
dataAttr: 'data-kc-target-id',
id: target.id,
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">&#x2605;</button>`,
removeOnclick: `deleteKCTarget('${target.id}')`,
removeTitle: t('common.delete'),
content: `

View File

@@ -12,6 +12,7 @@ import {
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
} from '../core/icons.js';
import { scenePresetsCache } from '../core/state.js';
import { cardColorStyle, cardColorButton } from '../core/card-colors.js';
let _editingId = null;
let _allTargets = []; // fetched on capture open
@@ -24,7 +25,6 @@ class ScenePresetEditorModal extends Modal {
return {
name: document.getElementById('scene-preset-editor-name').value,
description: document.getElementById('scene-preset-editor-description').value,
color: document.getElementById('scene-preset-editor-color').value,
targets: items,
};
}
@@ -40,7 +40,6 @@ export const csScenes = new CardSection('scenes', {
export function createSceneCard(preset) {
const targetCount = (preset.targets || []).length;
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
const meta = [
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
@@ -48,7 +47,8 @@ export function createSceneCard(preset) {
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
return `<div class="card" data-scene-id="${preset.id}" style="${colorStyle}">
const colorStyle = cardColorStyle(preset.id);
return `<div class="card" data-scene-id="${preset.id}"${colorStyle ? ` style="${colorStyle}"` : ''}>
<div class="card-top-actions">
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">&#x2715;</button>
</div>
@@ -64,6 +64,7 @@ export function createSceneCard(preset) {
<button class="btn btn-icon btn-secondary" onclick="editScenePreset('${preset.id}')" title="${t('scenes.edit')}">${ICON_EDIT}</button>
<button class="btn btn-icon btn-secondary" onclick="recaptureScenePreset('${preset.id}')" title="${t('scenes.recapture')}">${ICON_REFRESH}</button>
<button class="btn btn-icon btn-success" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
${cardColorButton(preset.id, 'data-scene-id')}
</div>
</div>`;
}
@@ -84,14 +85,14 @@ export function renderScenePresetsSection(presets) {
}
function _renderDashboardPresetCard(preset) {
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
const targetCount = (preset.targets || []).length;
const subtitle = [
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
].filter(Boolean).join(' \u00b7 ');
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
const pStyle = cardColorStyle(preset.id);
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}"${pStyle ? ` style="${pStyle}"` : ''}>
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_SCENE}</span>
<div>
@@ -113,7 +114,7 @@ export async function openScenePresetCapture() {
document.getElementById('scene-preset-editor-id').value = '';
document.getElementById('scene-preset-editor-name').value = '';
document.getElementById('scene-preset-editor-description').value = '';
document.getElementById('scene-preset-editor-color').value = '#4fc3f7';
document.getElementById('scene-preset-editor-error').style.display = 'none';
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
@@ -149,7 +150,6 @@ export async function editScenePreset(presetId) {
document.getElementById('scene-preset-editor-id').value = presetId;
document.getElementById('scene-preset-editor-name').value = preset.name;
document.getElementById('scene-preset-editor-description').value = preset.description || '';
document.getElementById('scene-preset-editor-color').value = preset.color || '#4fc3f7';
document.getElementById('scene-preset-editor-error').style.display = 'none';
// Hide target selector in edit mode (metadata only)
@@ -168,7 +168,6 @@ export async function editScenePreset(presetId) {
export async function saveScenePreset() {
const name = document.getElementById('scene-preset-editor-name').value.trim();
const description = document.getElementById('scene-preset-editor-description').value.trim();
const color = document.getElementById('scene-preset-editor-color').value;
const errorEl = document.getElementById('scene-preset-editor-error');
if (!name) {
@@ -182,14 +181,14 @@ export async function saveScenePreset() {
if (_editingId) {
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
method: 'PUT',
body: JSON.stringify({ name, description, color }),
body: JSON.stringify({ name, description }),
});
} else {
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
.map(el => el.dataset.targetId);
resp = await fetchWithAuth('/scene-presets', {
method: 'POST',
body: JSON.stringify({ name, description, color, target_ids }),
body: JSON.stringify({ name, description, target_ids }),
});
}

View File

@@ -864,7 +864,6 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
return wrapCard({
dataAttr: 'data-target-id',
id: target.id,
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">&#x2605;</button>`,
removeOnclick: `deleteTarget('${target.id}')`,
removeTitle: t('common.delete'),
content: `
@@ -1064,25 +1063,6 @@ export async function cloneTarget(targetId) {
}
}
export async function toggleTargetAutoStart(targetId, enable) {
try {
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
method: 'PUT',
body: JSON.stringify({ auto_start: enable }),
});
if (response.ok) {
showToast(t(enable ? 'autostart.toggle.enabled' : 'autostart.toggle.disabled'), 'success');
loadTargetsTab();
} else {
const error = await response.json();
showToast(error.detail || t('target.error.autostart_toggle_failed'), 'error');
}
} catch (error) {
console.error('Failed to toggle auto-start:', error);
showToast(t('target.error.autostart_toggle_failed'), 'error');
}
}
export async function deleteTarget(targetId) {
const confirmed = await showConfirm(t('targets.delete.confirm'));
if (!confirmed) return;

View File

@@ -56,7 +56,8 @@ const sourcesTourSteps = [
{ selector: '[data-stream-tab="static_image"]', textKey: 'tour.src.static', position: 'bottom' },
{ selector: '[data-stream-tab="processed"]', textKey: 'tour.src.processed', position: 'bottom' },
{ selector: '[data-stream-tab="audio"]', textKey: 'tour.src.audio', position: 'bottom' },
{ selector: '[data-stream-tab="value"]', textKey: 'tour.src.value', position: 'bottom' }
{ selector: '[data-stream-tab="value"]', textKey: 'tour.src.value', position: 'bottom' },
{ selector: '[data-stream-tab="sync"]', textKey: 'tour.src.sync', position: 'bottom' }
];
const automationsTutorialSteps = [

View File

@@ -262,6 +262,7 @@
"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 automations.",
"tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.",
"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.",
@@ -584,7 +585,9 @@
"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.always.hint": "Automation activates immediately when enabled and stays active.",
"automations.condition.startup": "Startup",
"automations.condition.startup.hint": "Activates when the server starts and stays active while enabled.",
"automations.condition.application": "Application",
"automations.condition.application.apps": "Applications:",
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
@@ -657,8 +660,6 @@
"scenes.name.placeholder": "My Scene",
"scenes.description": "Description:",
"scenes.description.hint": "Optional description of what this scene does",
"scenes.color": "Card Color:",
"scenes.color.hint": "Accent color for the scene card on the dashboard",
"scenes.targets": "Targets:",
"scenes.targets.hint": "Select which targets to include in this scene snapshot",
"scenes.capture": "Capture",
@@ -680,10 +681,6 @@
"scenes.error.activate_failed": "Failed to activate scene",
"scenes.error.recapture_failed": "Failed to recapture scene",
"scenes.error.delete_failed": "Failed to delete scene",
"autostart.title": "Auto-start Targets",
"autostart.toggle.enabled": "Auto-start enabled",
"autostart.toggle.disabled": "Auto-start disabled",
"autostart.goto_target": "Go to target",
"time.hours_minutes": "{h}h {m}m",
"time.minutes_seconds": "{m}m {s}s",
"time.seconds": "{s}s",
@@ -1110,13 +1107,11 @@
"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",
"dashboard.error.stop_all": "Failed to stop all targets",
"target.error.editor_open_failed": "Failed to open target editor",
"target.error.start_failed": "Failed to start target",
"target.error.stop_failed": "Failed to stop target",
"target.error.clone_failed": "Failed to clone target",
"target.error.autostart_toggle_failed": "Failed to toggle auto-start",
"target.error.delete_failed": "Failed to delete target",
"targets.stop_all.none_running": "No targets are currently running",
"targets.stop_all.stopped": "Stopped {count} target(s)",

View File

@@ -262,6 +262,7 @@
"tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.",
"tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.",
"tour.src.value": "Значения — числовые источники данных для условий автоматизаций.",
"tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.",
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
@@ -584,7 +585,9 @@
"automations.conditions.add": "Добавить условие",
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена",
"automations.condition.always": "Всегда",
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной. Используйте для автозапуска сцен при старте сервера.",
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.",
"automations.condition.startup": "Автозапуск",
"automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.",
"automations.condition.application": "Приложение",
"automations.condition.application.apps": "Приложения:",
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
@@ -657,8 +660,6 @@
"scenes.name.placeholder": "Моя сцена",
"scenes.description": "Описание:",
"scenes.description.hint": "Необязательное описание назначения этой сцены",
"scenes.color": "Цвет карточки:",
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
"scenes.targets": "Цели:",
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
"scenes.capture": "Захват",
@@ -680,10 +681,6 @@
"scenes.error.activate_failed": "Не удалось активировать сцену",
"scenes.error.recapture_failed": "Не удалось перезахватить сцену",
"scenes.error.delete_failed": "Не удалось удалить сцену",
"autostart.title": "Автозапуск целей",
"autostart.toggle.enabled": "Автозапуск включён",
"autostart.toggle.disabled": "Автозапуск отключён",
"autostart.goto_target": "Перейти к цели",
"time.hours_minutes": "{h}ч {m}м",
"time.minutes_seconds": "{m}м {s}с",
"time.seconds": "{s}с",
@@ -1110,13 +1107,11 @@
"dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию",
"dashboard.error.start_failed": "Не удалось запустить обработку",
"dashboard.error.stop_failed": "Не удалось остановить обработку",
"dashboard.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
"dashboard.error.stop_all": "Не удалось остановить все цели",
"target.error.editor_open_failed": "Не удалось открыть редактор цели",
"target.error.start_failed": "Не удалось запустить цель",
"target.error.stop_failed": "Не удалось остановить цель",
"target.error.clone_failed": "Не удалось клонировать цель",
"target.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
"target.error.delete_failed": "Не удалось удалить цель",
"targets.stop_all.none_running": "Нет запущенных целей",
"targets.stop_all.stopped": "Остановлено целей: {count}",

View File

@@ -262,6 +262,7 @@
"tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。",
"tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。",
"tour.src.value": "数值 — 用于自动化条件的数字数据源。",
"tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。",
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
@@ -584,7 +585,9 @@
"automations.conditions.add": "添加条件",
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态",
"automations.condition.always": "始终",
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。用于服务器启动时自动激活场景。",
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。",
"automations.condition.startup": "启动",
"automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。",
"automations.condition.application": "应用程序",
"automations.condition.application.apps": "应用程序:",
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe",
@@ -657,8 +660,6 @@
"scenes.name.placeholder": "我的场景",
"scenes.description": "描述:",
"scenes.description.hint": "此场景功能的可选描述",
"scenes.color": "卡片颜色:",
"scenes.color.hint": "仪表盘上场景卡片的强调色",
"scenes.targets": "目标:",
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
"scenes.capture": "捕获",
@@ -680,10 +681,6 @@
"scenes.error.activate_failed": "激活场景失败",
"scenes.error.recapture_failed": "重新捕获场景失败",
"scenes.error.delete_failed": "删除场景失败",
"autostart.title": "自动启动目标",
"autostart.toggle.enabled": "自动启动已启用",
"autostart.toggle.disabled": "自动启动已禁用",
"autostart.goto_target": "跳转到目标",
"time.hours_minutes": "{h}时 {m}分",
"time.minutes_seconds": "{m}分 {s}秒",
"time.seconds": "{s}秒",
@@ -1110,13 +1107,11 @@
"dashboard.error.automation_toggle_failed": "切换自动化失败",
"dashboard.error.start_failed": "启动处理失败",
"dashboard.error.stop_failed": "停止处理失败",
"dashboard.error.autostart_toggle_failed": "切换自动启动失败",
"dashboard.error.stop_all": "停止所有目标失败",
"target.error.editor_open_failed": "打开目标编辑器失败",
"target.error.start_failed": "启动目标失败",
"target.error.stop_failed": "停止目标失败",
"target.error.clone_failed": "克隆目标失败",
"target.error.autostart_toggle_failed": "切换自动启动失败",
"target.error.delete_failed": "删除目标失败",
"targets.stop_all.none_running": "当前没有运行中的目标",
"targets.stop_all.stopped": "已停止 {count} 个目标",

View File

@@ -7,7 +7,7 @@
* - Navigation: network-first with offline fallback
*/
const CACHE_NAME = 'ledgrab-v9';
const CACHE_NAME = 'ledgrab-v10';
// Only pre-cache static assets (no auth required).
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.