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:
@@ -24,6 +24,7 @@ from wled_controller.storage.automation import (
|
|||||||
Condition,
|
Condition,
|
||||||
DisplayStateCondition,
|
DisplayStateCondition,
|
||||||
MQTTCondition,
|
MQTTCondition,
|
||||||
|
StartupCondition,
|
||||||
SystemIdleCondition,
|
SystemIdleCondition,
|
||||||
TimeOfDayCondition,
|
TimeOfDayCondition,
|
||||||
WebhookCondition,
|
WebhookCondition,
|
||||||
@@ -70,6 +71,8 @@ def _condition_from_schema(s: ConditionSchema) -> Condition:
|
|||||||
return WebhookCondition(
|
return WebhookCondition(
|
||||||
token=s.token or secrets.token_hex(16),
|
token=s.token or secrets.token_hex(16),
|
||||||
)
|
)
|
||||||
|
if s.condition_type == "startup":
|
||||||
|
return StartupCondition()
|
||||||
raise ValueError(f"Unknown condition type: {s.condition_type}")
|
raise ValueError(f"Unknown condition type: {s.condition_type}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
adaptive_fps=target.adaptive_fps,
|
adaptive_fps=target.adaptive_fps,
|
||||||
protocol=target.protocol,
|
protocol=target.protocol,
|
||||||
description=target.description,
|
description=target.description,
|
||||||
auto_start=target.auto_start,
|
|
||||||
created_at=target.created_at,
|
created_at=target.created_at,
|
||||||
updated_at=target.updated_at,
|
updated_at=target.updated_at,
|
||||||
)
|
)
|
||||||
@@ -117,7 +117,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
picture_source_id=target.picture_source_id,
|
picture_source_id=target.picture_source_id,
|
||||||
key_colors_settings=_kc_settings_to_schema(target.settings),
|
key_colors_settings=_kc_settings_to_schema(target.settings),
|
||||||
description=target.description,
|
description=target.description,
|
||||||
auto_start=target.auto_start,
|
|
||||||
created_at=target.created_at,
|
created_at=target.created_at,
|
||||||
updated_at=target.updated_at,
|
updated_at=target.updated_at,
|
||||||
)
|
)
|
||||||
@@ -127,7 +127,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
name=target.name,
|
name=target.name,
|
||||||
target_type=target.target_type,
|
target_type=target.target_type,
|
||||||
description=target.description,
|
description=target.description,
|
||||||
auto_start=target.auto_start,
|
|
||||||
created_at=target.created_at,
|
created_at=target.created_at,
|
||||||
updated_at=target.updated_at,
|
updated_at=target.updated_at,
|
||||||
)
|
)
|
||||||
@@ -169,7 +169,6 @@ async def create_target(
|
|||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
key_colors_settings=kc_settings,
|
key_colors_settings=kc_settings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
auto_start=data.auto_start,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register in processor manager
|
# Register in processor manager
|
||||||
@@ -288,7 +287,6 @@ async def update_target(
|
|||||||
protocol=data.protocol,
|
protocol=data.protocol,
|
||||||
key_colors_settings=kc_settings,
|
key_colors_settings=kc_settings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
auto_start=data.auto_start,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Detect KC brightness VS change (inside key_colors_settings)
|
# Detect KC brightness VS change (inside key_colors_settings)
|
||||||
|
|||||||
@@ -37,14 +37,12 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
|||||||
id=preset.id,
|
id=preset.id,
|
||||||
name=preset.name,
|
name=preset.name,
|
||||||
description=preset.description,
|
description=preset.description,
|
||||||
color=preset.color,
|
|
||||||
targets=[{
|
targets=[{
|
||||||
"target_id": t.target_id,
|
"target_id": t.target_id,
|
||||||
"running": t.running,
|
"running": t.running,
|
||||||
"color_strip_source_id": t.color_strip_source_id,
|
"color_strip_source_id": t.color_strip_source_id,
|
||||||
"brightness_value_source_id": t.brightness_value_source_id,
|
"brightness_value_source_id": t.brightness_value_source_id,
|
||||||
"fps": t.fps,
|
"fps": t.fps,
|
||||||
"auto_start": t.auto_start,
|
|
||||||
} for t in preset.targets],
|
} for t in preset.targets],
|
||||||
order=preset.order,
|
order=preset.order,
|
||||||
created_at=preset.created_at,
|
created_at=preset.created_at,
|
||||||
@@ -76,7 +74,6 @@ async def create_scene_preset(
|
|||||||
id=f"scene_{uuid.uuid4().hex[:8]}",
|
id=f"scene_{uuid.uuid4().hex[:8]}",
|
||||||
name=data.name,
|
name=data.name,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
color=data.color,
|
|
||||||
targets=targets,
|
targets=targets,
|
||||||
order=store.count(),
|
order=store.count(),
|
||||||
created_at=now,
|
created_at=now,
|
||||||
@@ -143,7 +140,6 @@ async def update_scene_preset(
|
|||||||
preset_id,
|
preset_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
color=data.color,
|
|
||||||
order=data.order,
|
order=data.order,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class PictureTargetCreate(BaseModel):
|
|||||||
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
auto_start: bool = Field(default=False, description="Auto-start on server boot")
|
|
||||||
|
|
||||||
|
|
||||||
class PictureTargetUpdate(BaseModel):
|
class PictureTargetUpdate(BaseModel):
|
||||||
@@ -86,7 +85,6 @@ class PictureTargetUpdate(BaseModel):
|
|||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
auto_start: Optional[bool] = Field(None, description="Auto-start on server boot")
|
|
||||||
|
|
||||||
|
|
||||||
class PictureTargetResponse(BaseModel):
|
class PictureTargetResponse(BaseModel):
|
||||||
@@ -109,7 +107,6 @@ class PictureTargetResponse(BaseModel):
|
|||||||
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
auto_start: bool = Field(default=False, description="Auto-start on server boot")
|
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class TargetSnapshotSchema(BaseModel):
|
|||||||
color_strip_source_id: str = ""
|
color_strip_source_id: str = ""
|
||||||
brightness_value_source_id: str = ""
|
brightness_value_source_id: str = ""
|
||||||
fps: int = 30
|
fps: int = 30
|
||||||
auto_start: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class ScenePresetCreate(BaseModel):
|
class ScenePresetCreate(BaseModel):
|
||||||
@@ -20,7 +19,6 @@ class ScenePresetCreate(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||||
description: str = Field(default="", max_length=500)
|
description: str = Field(default="", max_length=500)
|
||||||
color: str = Field(default="#4fc3f7", description="Card accent color")
|
|
||||||
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
|
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +27,6 @@ class ScenePresetUpdate(BaseModel):
|
|||||||
|
|
||||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||||
description: Optional[str] = Field(None, max_length=500)
|
description: Optional[str] = Field(None, max_length=500)
|
||||||
color: Optional[str] = None
|
|
||||||
order: Optional[int] = None
|
order: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +36,6 @@ class ScenePresetResponse(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
color: str
|
|
||||||
targets: List[TargetSnapshotSchema]
|
targets: List[TargetSnapshotSchema]
|
||||||
order: int
|
order: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from wled_controller.storage.automation import (
|
|||||||
Condition,
|
Condition,
|
||||||
DisplayStateCondition,
|
DisplayStateCondition,
|
||||||
MQTTCondition,
|
MQTTCondition,
|
||||||
|
StartupCondition,
|
||||||
SystemIdleCondition,
|
SystemIdleCondition,
|
||||||
TimeOfDayCondition,
|
TimeOfDayCondition,
|
||||||
WebhookCondition,
|
WebhookCondition,
|
||||||
@@ -204,7 +205,7 @@ class AutomationEngine:
|
|||||||
fullscreen_procs: Set[str],
|
fullscreen_procs: Set[str],
|
||||||
idle_seconds: Optional[float], display_state: Optional[str],
|
idle_seconds: Optional[float], display_state: Optional[str],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if isinstance(condition, AlwaysCondition):
|
if isinstance(condition, (AlwaysCondition, StartupCondition)):
|
||||||
return True
|
return True
|
||||||
if isinstance(condition, ApplicationCondition):
|
if isinstance(condition, ApplicationCondition):
|
||||||
return self._evaluate_app_condition(condition, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs)
|
return self._evaluate_app_condition(condition, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs)
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ def capture_current_snapshot(
|
|||||||
color_strip_source_id=getattr(t, "color_strip_source_id", ""),
|
color_strip_source_id=getattr(t, "color_strip_source_id", ""),
|
||||||
brightness_value_source_id=getattr(t, "brightness_value_source_id", ""),
|
brightness_value_source_id=getattr(t, "brightness_value_source_id", ""),
|
||||||
fps=getattr(t, "fps", 30),
|
fps=getattr(t, "fps", 30),
|
||||||
auto_start=getattr(t, "auto_start", False),
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
@@ -83,9 +82,6 @@ async def apply_scene_state(
|
|||||||
changed["brightness_value_source_id"] = ts.brightness_value_source_id
|
changed["brightness_value_source_id"] = ts.brightness_value_source_id
|
||||||
if getattr(target, "fps", None) != ts.fps:
|
if getattr(target, "fps", None) != ts.fps:
|
||||||
changed["fps"] = ts.fps
|
changed["fps"] = ts.fps
|
||||||
if getattr(target, "auto_start", None) != ts.auto_start:
|
|
||||||
changed["auto_start"] = ts.auto_start
|
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
target.update_fields(**changed)
|
target.update_fields(**changed)
|
||||||
target_store.update_target(ts.target_id, **changed)
|
target_store.update_target(ts.target_id, **changed)
|
||||||
|
|||||||
@@ -188,19 +188,6 @@ async def lifespan(app: FastAPI):
|
|||||||
# Start auto-backup engine (periodic configuration backups)
|
# Start auto-backup engine (periodic configuration backups)
|
||||||
await auto_backup_engine.start()
|
await auto_backup_engine.start()
|
||||||
|
|
||||||
# Auto-start targets with auto_start=True
|
|
||||||
auto_started = 0
|
|
||||||
for target in targets:
|
|
||||||
if getattr(target, "auto_start", False):
|
|
||||||
try:
|
|
||||||
await processor_manager.start_processing(target.id)
|
|
||||||
auto_started += 1
|
|
||||||
logger.info(f"Auto-started target: {target.name} ({target.id})")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to auto-start target {target.id}: {e}")
|
|
||||||
if auto_started:
|
|
||||||
logger.info(f"Auto-started {auto_started} target(s)")
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from './features/devices.js';
|
} from './features/devices.js';
|
||||||
import {
|
import {
|
||||||
loadDashboard, stopUptimeTimer,
|
loadDashboard, stopUptimeTimer,
|
||||||
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
|
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
|
||||||
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
|
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
|
||||||
toggleDashboardSection, changeDashboardPollInterval,
|
toggleDashboardSection, changeDashboardPollInterval,
|
||||||
} from './features/dashboard.js';
|
} from './features/dashboard.js';
|
||||||
@@ -100,7 +100,7 @@ import {
|
|||||||
startTargetProcessing, stopTargetProcessing,
|
startTargetProcessing, stopTargetProcessing,
|
||||||
stopAllLedTargets, stopAllKCTargets,
|
stopAllLedTargets, stopAllKCTargets,
|
||||||
startTargetOverlay, stopTargetOverlay, deleteTarget,
|
startTargetOverlay, stopTargetOverlay, deleteTarget,
|
||||||
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
|
cloneTarget, toggleLedPreview,
|
||||||
expandAllTargetSections, collapseAllTargetSections,
|
expandAllTargetSections, collapseAllTargetSections,
|
||||||
disconnectAllLedPreviewWS,
|
disconnectAllLedPreviewWS,
|
||||||
} from './features/targets.js';
|
} from './features/targets.js';
|
||||||
@@ -211,7 +211,6 @@ Object.assign(window, {
|
|||||||
dashboardToggleAutomation,
|
dashboardToggleAutomation,
|
||||||
dashboardStartTarget,
|
dashboardStartTarget,
|
||||||
dashboardStopTarget,
|
dashboardStopTarget,
|
||||||
dashboardToggleAutoStart,
|
|
||||||
dashboardStopAll,
|
dashboardStopAll,
|
||||||
dashboardPauseClock,
|
dashboardPauseClock,
|
||||||
dashboardResumeClock,
|
dashboardResumeClock,
|
||||||
@@ -356,7 +355,6 @@ Object.assign(window, {
|
|||||||
deleteTarget,
|
deleteTarget,
|
||||||
cloneTarget,
|
cloneTarget,
|
||||||
toggleLedPreview,
|
toggleLedPreview,
|
||||||
toggleTargetAutoStart,
|
|
||||||
disconnectAllLedPreviewWS,
|
disconnectAllLedPreviewWS,
|
||||||
|
|
||||||
// color-strip sources
|
// color-strip sources
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ export function cardColorButton(entityId, cardAttr) {
|
|||||||
|
|
||||||
registerColorPicker(pickerId, (hex) => {
|
registerColorPicker(pickerId, (hex) => {
|
||||||
setCardColor(entityId, 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}` : '';
|
if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
|||||||
if (c.condition_type === 'always') {
|
if (c.condition_type === 'always') {
|
||||||
return `<span class="stream-card-prop">${ICON_OK} ${t('automations.condition.always')}</span>`;
|
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') {
|
if (c.condition_type === 'application') {
|
||||||
const apps = (c.apps || []).join(', ');
|
const apps = (c.apps || []).join(', ');
|
||||||
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
|
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
|
||||||
@@ -384,6 +387,7 @@ function addAutomationConditionRow(condition) {
|
|||||||
<div class="condition-header">
|
<div class="condition-header">
|
||||||
<select class="condition-type-select">
|
<select class="condition-type-select">
|
||||||
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('automations.condition.always')}</option>
|
<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="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="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="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>`;
|
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.always.hint')}</small>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type === 'startup') {
|
||||||
|
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.startup.hint')}</small>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type === 'time_of_day') {
|
if (type === 'time_of_day') {
|
||||||
const startTime = data.start_time || '00:00';
|
const startTime = data.start_time || '00:00';
|
||||||
const endTime = data.end_time || '23:59';
|
const endTime = data.end_time || '23:59';
|
||||||
@@ -612,6 +620,8 @@ function getAutomationEditorConditions() {
|
|||||||
const condType = typeSelect ? typeSelect.value : 'application';
|
const condType = typeSelect ? typeSelect.value : 'application';
|
||||||
if (condType === 'always') {
|
if (condType === 'always') {
|
||||||
conditions.push({ condition_type: 'always' });
|
conditions.push({ condition_type: 'always' });
|
||||||
|
} else if (condType === 'startup') {
|
||||||
|
conditions.push({ condition_type: 'startup' });
|
||||||
} else if (condType === 'time_of_day') {
|
} else if (condType === 'time_of_day') {
|
||||||
conditions.push({
|
conditions.push({
|
||||||
condition_type: 'time_of_day',
|
condition_type: 'time_of_day',
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling }
|
|||||||
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
||||||
import {
|
import {
|
||||||
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
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';
|
} from '../core/icons.js';
|
||||||
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
||||||
|
import { cardColorStyle } from '../core/card-colors.js';
|
||||||
|
|
||||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||||
const MAX_FPS_SAMPLES = 120;
|
const MAX_FPS_SAMPLES = 120;
|
||||||
@@ -21,7 +22,6 @@ let _fpsHistory = {}; // { targetId: number[] } — fps_actual
|
|||||||
let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
|
let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
|
||||||
let _fpsCharts = {}; // { targetId: Chart }
|
let _fpsCharts = {}; // { targetId: Chart }
|
||||||
let _lastRunningIds = []; // sorted target IDs from previous render
|
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 _lastSyncClockIds = ''; // comma-joined sorted sync clock IDs
|
||||||
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
||||||
let _uptimeTimer = null;
|
let _uptimeTimer = null;
|
||||||
@@ -308,7 +308,8 @@ function renderDashboardSyncClock(clock) {
|
|||||||
clock.description ? escapeHtml(clock.description) : '',
|
clock.description ? escapeHtml(clock.description) : '',
|
||||||
].filter(Boolean).join(' · ');
|
].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">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">${ICON_CLOCK}</span>
|
<span class="dashboard-target-icon">${ICON_CLOCK}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -447,8 +448,6 @@ export async function loadDashboard(forceFullRender = false) {
|
|||||||
// Build dynamic HTML (targets, automations)
|
// Build dynamic HTML (targets, automations)
|
||||||
let dynamicHtml = '';
|
let dynamicHtml = '';
|
||||||
let runningIds = [];
|
let runningIds = [];
|
||||||
let newAutoStartIds = '';
|
|
||||||
|
|
||||||
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
|
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
|
||||||
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
||||||
} else {
|
} 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)
|
// 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 newRunningIds = running.map(t => t.id).sort().join(',');
|
||||||
const prevRunningIds = [..._lastRunningIds].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 newSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
||||||
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
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) {
|
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
||||||
_updateRunningMetrics(running);
|
_updateRunningMetrics(running);
|
||||||
_updateSyncClocksInPlace(syncClocks);
|
_updateSyncClocksInPlace(syncClocks);
|
||||||
@@ -489,52 +487,6 @@ export async function loadDashboard(forceFullRender = false) {
|
|||||||
return;
|
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) {
|
if (automations.length > 0) {
|
||||||
const activeAutomations = automations.filter(a => a.is_active);
|
const activeAutomations = automations.filter(a => a.is_active);
|
||||||
const inactiveAutomations = 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;
|
_lastRunningIds = runningIds;
|
||||||
_lastAutoStartIds = newAutoStartIds;
|
|
||||||
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
||||||
_cacheUptimeElements();
|
_cacheUptimeElements();
|
||||||
await _initFpsCharts(runningIds);
|
await _initFpsCharts(runningIds);
|
||||||
@@ -683,7 +634,8 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
healthDot = `<span class="health-dot ${cls}"></span>`;
|
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">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">${icon}</span>
|
<span class="dashboard-target-icon">${icon}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -708,12 +660,12 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<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')}">★</button>
|
|
||||||
<button class="dashboard-action-btn stop" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">${ICON_STOP_PLAIN}</button>
|
<button class="dashboard-action-btn stop" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">${ICON_STOP_PLAIN}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} 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">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">${icon}</span>
|
<span class="dashboard-target-icon">${icon}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -723,7 +675,6 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-metrics"></div>
|
<div class="dashboard-target-metrics"></div>
|
||||||
<div class="dashboard-target-actions">
|
<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')}">★</button>
|
|
||||||
<button class="dashboard-action-btn start" onclick="dashboardStartTarget('${target.id}')" title="${t('device.button.start')}">${ICON_START}</button>
|
<button class="dashboard-action-btn start" onclick="dashboardStartTarget('${target.id}')" title="${t('device.button.start')}">${ICON_START}</button>
|
||||||
</div>
|
</div>
|
||||||
</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');
|
const matchLabel = c.match_type === 'topmost' ? t('automations.condition.application.match_type.topmost') : t('automations.condition.application.match_type.running');
|
||||||
return `${apps} (${matchLabel})`;
|
return `${apps} (${matchLabel})`;
|
||||||
}
|
}
|
||||||
|
if (c.condition_type === 'startup') return t('automations.condition.startup');
|
||||||
return c.condition_type;
|
return c.condition_type;
|
||||||
});
|
});
|
||||||
const logic = automation.condition_logic === 'and' ? ' & ' : ' | ';
|
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 scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null;
|
||||||
const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected');
|
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">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">${ICON_AUTOMATION}</span>
|
<span class="dashboard-target-icon">${ICON_AUTOMATION}</span>
|
||||||
<div>
|
<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() {
|
export async function dashboardStopAll() {
|
||||||
try {
|
try {
|
||||||
const [targetsResp, statesResp] = await Promise.all([
|
const [targetsResp, statesResp] = await Promise.all([
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
return wrapCard({
|
return wrapCard({
|
||||||
dataAttr: 'data-kc-target-id',
|
dataAttr: 'data-kc-target-id',
|
||||||
id: 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')}">★</button>`,
|
|
||||||
removeOnclick: `deleteKCTarget('${target.id}')`,
|
removeOnclick: `deleteKCTarget('${target.id}')`,
|
||||||
removeTitle: t('common.delete'),
|
removeTitle: t('common.delete'),
|
||||||
content: `
|
content: `
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
import { scenePresetsCache } from '../core/state.js';
|
import { scenePresetsCache } from '../core/state.js';
|
||||||
|
import { cardColorStyle, cardColorButton } from '../core/card-colors.js';
|
||||||
|
|
||||||
let _editingId = null;
|
let _editingId = null;
|
||||||
let _allTargets = []; // fetched on capture open
|
let _allTargets = []; // fetched on capture open
|
||||||
@@ -24,7 +25,6 @@ class ScenePresetEditorModal extends Modal {
|
|||||||
return {
|
return {
|
||||||
name: document.getElementById('scene-preset-editor-name').value,
|
name: document.getElementById('scene-preset-editor-name').value,
|
||||||
description: document.getElementById('scene-preset-editor-description').value,
|
description: document.getElementById('scene-preset-editor-description').value,
|
||||||
color: document.getElementById('scene-preset-editor-color').value,
|
|
||||||
targets: items,
|
targets: items,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,6 @@ export const csScenes = new CardSection('scenes', {
|
|||||||
|
|
||||||
export function createSceneCard(preset) {
|
export function createSceneCard(preset) {
|
||||||
const targetCount = (preset.targets || []).length;
|
const targetCount = (preset.targets || []).length;
|
||||||
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
|
||||||
|
|
||||||
const meta = [
|
const meta = [
|
||||||
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
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() : '';
|
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">
|
<div class="card-top-actions">
|
||||||
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">✕</button>
|
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">✕</button>
|
||||||
</div>
|
</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="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-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>
|
<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>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -84,14 +85,14 @@ export function renderScenePresetsSection(presets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _renderDashboardPresetCard(preset) {
|
function _renderDashboardPresetCard(preset) {
|
||||||
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
|
||||||
const targetCount = (preset.targets || []).length;
|
const targetCount = (preset.targets || []).length;
|
||||||
|
|
||||||
const subtitle = [
|
const subtitle = [
|
||||||
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
||||||
].filter(Boolean).join(' \u00b7 ');
|
].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">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -113,7 +114,7 @@ export async function openScenePresetCapture() {
|
|||||||
document.getElementById('scene-preset-editor-id').value = '';
|
document.getElementById('scene-preset-editor-id').value = '';
|
||||||
document.getElementById('scene-preset-editor-name').value = '';
|
document.getElementById('scene-preset-editor-name').value = '';
|
||||||
document.getElementById('scene-preset-editor-description').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';
|
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||||
|
|
||||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
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-id').value = presetId;
|
||||||
document.getElementById('scene-preset-editor-name').value = preset.name;
|
document.getElementById('scene-preset-editor-name').value = preset.name;
|
||||||
document.getElementById('scene-preset-editor-description').value = preset.description || '';
|
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';
|
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||||
|
|
||||||
// Hide target selector in edit mode (metadata only)
|
// Hide target selector in edit mode (metadata only)
|
||||||
@@ -168,7 +168,6 @@ export async function editScenePreset(presetId) {
|
|||||||
export async function saveScenePreset() {
|
export async function saveScenePreset() {
|
||||||
const name = document.getElementById('scene-preset-editor-name').value.trim();
|
const name = document.getElementById('scene-preset-editor-name').value.trim();
|
||||||
const description = document.getElementById('scene-preset-editor-description').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');
|
const errorEl = document.getElementById('scene-preset-editor-error');
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -182,14 +181,14 @@ export async function saveScenePreset() {
|
|||||||
if (_editingId) {
|
if (_editingId) {
|
||||||
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
|
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ name, description, color }),
|
body: JSON.stringify({ name, description }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||||
.map(el => el.dataset.targetId);
|
.map(el => el.dataset.targetId);
|
||||||
resp = await fetchWithAuth('/scene-presets', {
|
resp = await fetchWithAuth('/scene-presets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name, description, color, target_ids }),
|
body: JSON.stringify({ name, description, target_ids }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -864,7 +864,6 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
return wrapCard({
|
return wrapCard({
|
||||||
dataAttr: 'data-target-id',
|
dataAttr: 'data-target-id',
|
||||||
id: 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')}">★</button>`,
|
|
||||||
removeOnclick: `deleteTarget('${target.id}')`,
|
removeOnclick: `deleteTarget('${target.id}')`,
|
||||||
removeTitle: t('common.delete'),
|
removeTitle: t('common.delete'),
|
||||||
content: `
|
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) {
|
export async function deleteTarget(targetId) {
|
||||||
const confirmed = await showConfirm(t('targets.delete.confirm'));
|
const confirmed = await showConfirm(t('targets.delete.confirm'));
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ const sourcesTourSteps = [
|
|||||||
{ selector: '[data-stream-tab="static_image"]', textKey: 'tour.src.static', position: 'bottom' },
|
{ 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="processed"]', textKey: 'tour.src.processed', position: 'bottom' },
|
||||||
{ selector: '[data-stream-tab="audio"]', textKey: 'tour.src.audio', 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 = [
|
const automationsTutorialSteps = [
|
||||||
|
|||||||
@@ -262,6 +262,7 @@
|
|||||||
"tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.",
|
"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.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.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.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.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.",
|
"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.add": "Add Condition",
|
||||||
"automations.conditions.empty": "No conditions — automation is always active when enabled",
|
"automations.conditions.empty": "No conditions — automation is always active when enabled",
|
||||||
"automations.condition.always": "Always",
|
"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": "Application",
|
||||||
"automations.condition.application.apps": "Applications:",
|
"automations.condition.application.apps": "Applications:",
|
||||||
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
||||||
@@ -657,8 +660,6 @@
|
|||||||
"scenes.name.placeholder": "My Scene",
|
"scenes.name.placeholder": "My Scene",
|
||||||
"scenes.description": "Description:",
|
"scenes.description": "Description:",
|
||||||
"scenes.description.hint": "Optional description of what this scene does",
|
"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": "Targets:",
|
||||||
"scenes.targets.hint": "Select which targets to include in this scene snapshot",
|
"scenes.targets.hint": "Select which targets to include in this scene snapshot",
|
||||||
"scenes.capture": "Capture",
|
"scenes.capture": "Capture",
|
||||||
@@ -680,10 +681,6 @@
|
|||||||
"scenes.error.activate_failed": "Failed to activate scene",
|
"scenes.error.activate_failed": "Failed to activate scene",
|
||||||
"scenes.error.recapture_failed": "Failed to recapture scene",
|
"scenes.error.recapture_failed": "Failed to recapture scene",
|
||||||
"scenes.error.delete_failed": "Failed to delete 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.hours_minutes": "{h}h {m}m",
|
||||||
"time.minutes_seconds": "{m}m {s}s",
|
"time.minutes_seconds": "{m}m {s}s",
|
||||||
"time.seconds": "{s}s",
|
"time.seconds": "{s}s",
|
||||||
@@ -1110,13 +1107,11 @@
|
|||||||
"dashboard.error.automation_toggle_failed": "Failed to toggle automation",
|
"dashboard.error.automation_toggle_failed": "Failed to toggle automation",
|
||||||
"dashboard.error.start_failed": "Failed to start processing",
|
"dashboard.error.start_failed": "Failed to start processing",
|
||||||
"dashboard.error.stop_failed": "Failed to stop 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",
|
"dashboard.error.stop_all": "Failed to stop all targets",
|
||||||
"target.error.editor_open_failed": "Failed to open target editor",
|
"target.error.editor_open_failed": "Failed to open target editor",
|
||||||
"target.error.start_failed": "Failed to start target",
|
"target.error.start_failed": "Failed to start target",
|
||||||
"target.error.stop_failed": "Failed to stop target",
|
"target.error.stop_failed": "Failed to stop target",
|
||||||
"target.error.clone_failed": "Failed to clone 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",
|
"target.error.delete_failed": "Failed to delete target",
|
||||||
"targets.stop_all.none_running": "No targets are currently running",
|
"targets.stop_all.none_running": "No targets are currently running",
|
||||||
"targets.stop_all.stopped": "Stopped {count} target(s)",
|
"targets.stop_all.stopped": "Stopped {count} target(s)",
|
||||||
|
|||||||
@@ -262,6 +262,7 @@
|
|||||||
"tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.",
|
"tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.",
|
||||||
"tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.",
|
"tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.",
|
||||||
"tour.src.value": "Значения — числовые источники данных для условий автоматизаций.",
|
"tour.src.value": "Значения — числовые источники данных для условий автоматизаций.",
|
||||||
|
"tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.",
|
||||||
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
|
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
|
||||||
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
|
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
|
||||||
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
|
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
|
||||||
@@ -584,7 +585,9 @@
|
|||||||
"automations.conditions.add": "Добавить условие",
|
"automations.conditions.add": "Добавить условие",
|
||||||
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена",
|
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена",
|
||||||
"automations.condition.always": "Всегда",
|
"automations.condition.always": "Всегда",
|
||||||
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной. Используйте для автозапуска сцен при старте сервера.",
|
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.",
|
||||||
|
"automations.condition.startup": "Автозапуск",
|
||||||
|
"automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.",
|
||||||
"automations.condition.application": "Приложение",
|
"automations.condition.application": "Приложение",
|
||||||
"automations.condition.application.apps": "Приложения:",
|
"automations.condition.application.apps": "Приложения:",
|
||||||
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
|
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
|
||||||
@@ -657,8 +660,6 @@
|
|||||||
"scenes.name.placeholder": "Моя сцена",
|
"scenes.name.placeholder": "Моя сцена",
|
||||||
"scenes.description": "Описание:",
|
"scenes.description": "Описание:",
|
||||||
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
||||||
"scenes.color": "Цвет карточки:",
|
|
||||||
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
|
|
||||||
"scenes.targets": "Цели:",
|
"scenes.targets": "Цели:",
|
||||||
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
|
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
|
||||||
"scenes.capture": "Захват",
|
"scenes.capture": "Захват",
|
||||||
@@ -680,10 +681,6 @@
|
|||||||
"scenes.error.activate_failed": "Не удалось активировать сцену",
|
"scenes.error.activate_failed": "Не удалось активировать сцену",
|
||||||
"scenes.error.recapture_failed": "Не удалось перезахватить сцену",
|
"scenes.error.recapture_failed": "Не удалось перезахватить сцену",
|
||||||
"scenes.error.delete_failed": "Не удалось удалить сцену",
|
"scenes.error.delete_failed": "Не удалось удалить сцену",
|
||||||
"autostart.title": "Автозапуск целей",
|
|
||||||
"autostart.toggle.enabled": "Автозапуск включён",
|
|
||||||
"autostart.toggle.disabled": "Автозапуск отключён",
|
|
||||||
"autostart.goto_target": "Перейти к цели",
|
|
||||||
"time.hours_minutes": "{h}ч {m}м",
|
"time.hours_minutes": "{h}ч {m}м",
|
||||||
"time.minutes_seconds": "{m}м {s}с",
|
"time.minutes_seconds": "{m}м {s}с",
|
||||||
"time.seconds": "{s}с",
|
"time.seconds": "{s}с",
|
||||||
@@ -1110,13 +1107,11 @@
|
|||||||
"dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию",
|
"dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию",
|
||||||
"dashboard.error.start_failed": "Не удалось запустить обработку",
|
"dashboard.error.start_failed": "Не удалось запустить обработку",
|
||||||
"dashboard.error.stop_failed": "Не удалось остановить обработку",
|
"dashboard.error.stop_failed": "Не удалось остановить обработку",
|
||||||
"dashboard.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
|
|
||||||
"dashboard.error.stop_all": "Не удалось остановить все цели",
|
"dashboard.error.stop_all": "Не удалось остановить все цели",
|
||||||
"target.error.editor_open_failed": "Не удалось открыть редактор цели",
|
"target.error.editor_open_failed": "Не удалось открыть редактор цели",
|
||||||
"target.error.start_failed": "Не удалось запустить цель",
|
"target.error.start_failed": "Не удалось запустить цель",
|
||||||
"target.error.stop_failed": "Не удалось остановить цель",
|
"target.error.stop_failed": "Не удалось остановить цель",
|
||||||
"target.error.clone_failed": "Не удалось клонировать цель",
|
"target.error.clone_failed": "Не удалось клонировать цель",
|
||||||
"target.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
|
|
||||||
"target.error.delete_failed": "Не удалось удалить цель",
|
"target.error.delete_failed": "Не удалось удалить цель",
|
||||||
"targets.stop_all.none_running": "Нет запущенных целей",
|
"targets.stop_all.none_running": "Нет запущенных целей",
|
||||||
"targets.stop_all.stopped": "Остановлено целей: {count}",
|
"targets.stop_all.stopped": "Остановлено целей: {count}",
|
||||||
|
|||||||
@@ -262,6 +262,7 @@
|
|||||||
"tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。",
|
"tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。",
|
||||||
"tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。",
|
"tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。",
|
||||||
"tour.src.value": "数值 — 用于自动化条件的数字数据源。",
|
"tour.src.value": "数值 — 用于自动化条件的数字数据源。",
|
||||||
|
"tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。",
|
||||||
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
|
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
|
||||||
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
|
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
|
||||||
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
|
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
|
||||||
@@ -584,7 +585,9 @@
|
|||||||
"automations.conditions.add": "添加条件",
|
"automations.conditions.add": "添加条件",
|
||||||
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态",
|
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态",
|
||||||
"automations.condition.always": "始终",
|
"automations.condition.always": "始终",
|
||||||
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。用于服务器启动时自动激活场景。",
|
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。",
|
||||||
|
"automations.condition.startup": "启动",
|
||||||
|
"automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。",
|
||||||
"automations.condition.application": "应用程序",
|
"automations.condition.application": "应用程序",
|
||||||
"automations.condition.application.apps": "应用程序:",
|
"automations.condition.application.apps": "应用程序:",
|
||||||
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)",
|
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)",
|
||||||
@@ -657,8 +660,6 @@
|
|||||||
"scenes.name.placeholder": "我的场景",
|
"scenes.name.placeholder": "我的场景",
|
||||||
"scenes.description": "描述:",
|
"scenes.description": "描述:",
|
||||||
"scenes.description.hint": "此场景功能的可选描述",
|
"scenes.description.hint": "此场景功能的可选描述",
|
||||||
"scenes.color": "卡片颜色:",
|
|
||||||
"scenes.color.hint": "仪表盘上场景卡片的强调色",
|
|
||||||
"scenes.targets": "目标:",
|
"scenes.targets": "目标:",
|
||||||
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
|
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
|
||||||
"scenes.capture": "捕获",
|
"scenes.capture": "捕获",
|
||||||
@@ -680,10 +681,6 @@
|
|||||||
"scenes.error.activate_failed": "激活场景失败",
|
"scenes.error.activate_failed": "激活场景失败",
|
||||||
"scenes.error.recapture_failed": "重新捕获场景失败",
|
"scenes.error.recapture_failed": "重新捕获场景失败",
|
||||||
"scenes.error.delete_failed": "删除场景失败",
|
"scenes.error.delete_failed": "删除场景失败",
|
||||||
"autostart.title": "自动启动目标",
|
|
||||||
"autostart.toggle.enabled": "自动启动已启用",
|
|
||||||
"autostart.toggle.disabled": "自动启动已禁用",
|
|
||||||
"autostart.goto_target": "跳转到目标",
|
|
||||||
"time.hours_minutes": "{h}时 {m}分",
|
"time.hours_minutes": "{h}时 {m}分",
|
||||||
"time.minutes_seconds": "{m}分 {s}秒",
|
"time.minutes_seconds": "{m}分 {s}秒",
|
||||||
"time.seconds": "{s}秒",
|
"time.seconds": "{s}秒",
|
||||||
@@ -1110,13 +1107,11 @@
|
|||||||
"dashboard.error.automation_toggle_failed": "切换自动化失败",
|
"dashboard.error.automation_toggle_failed": "切换自动化失败",
|
||||||
"dashboard.error.start_failed": "启动处理失败",
|
"dashboard.error.start_failed": "启动处理失败",
|
||||||
"dashboard.error.stop_failed": "停止处理失败",
|
"dashboard.error.stop_failed": "停止处理失败",
|
||||||
"dashboard.error.autostart_toggle_failed": "切换自动启动失败",
|
|
||||||
"dashboard.error.stop_all": "停止所有目标失败",
|
"dashboard.error.stop_all": "停止所有目标失败",
|
||||||
"target.error.editor_open_failed": "打开目标编辑器失败",
|
"target.error.editor_open_failed": "打开目标编辑器失败",
|
||||||
"target.error.start_failed": "启动目标失败",
|
"target.error.start_failed": "启动目标失败",
|
||||||
"target.error.stop_failed": "停止目标失败",
|
"target.error.stop_failed": "停止目标失败",
|
||||||
"target.error.clone_failed": "克隆目标失败",
|
"target.error.clone_failed": "克隆目标失败",
|
||||||
"target.error.autostart_toggle_failed": "切换自动启动失败",
|
|
||||||
"target.error.delete_failed": "删除目标失败",
|
"target.error.delete_failed": "删除目标失败",
|
||||||
"targets.stop_all.none_running": "当前没有运行中的目标",
|
"targets.stop_all.none_running": "当前没有运行中的目标",
|
||||||
"targets.stop_all.stopped": "已停止 {count} 个目标",
|
"targets.stop_all.stopped": "已停止 {count} 个目标",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Navigation: network-first with offline fallback
|
* - Navigation: network-first with offline fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'ledgrab-v9';
|
const CACHE_NAME = 'ledgrab-v10';
|
||||||
|
|
||||||
// Only pre-cache static assets (no auth required).
|
// Only pre-cache static assets (no auth required).
|
||||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class Condition:
|
|||||||
return MQTTCondition.from_dict(data)
|
return MQTTCondition.from_dict(data)
|
||||||
if ct == "webhook":
|
if ct == "webhook":
|
||||||
return WebhookCondition.from_dict(data)
|
return WebhookCondition.from_dict(data)
|
||||||
|
if ct == "startup":
|
||||||
|
return StartupCondition.from_dict(data)
|
||||||
raise ValueError(f"Unknown condition type: {ct}")
|
raise ValueError(f"Unknown condition type: {ct}")
|
||||||
|
|
||||||
|
|
||||||
@@ -177,6 +179,17 @@ class WebhookCondition(Condition):
|
|||||||
return cls(token=data.get("token", ""))
|
return cls(token=data.get("token", ""))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StartupCondition(Condition):
|
||||||
|
"""Activate when the server starts — stays active while enabled."""
|
||||||
|
|
||||||
|
condition_type: str = "startup"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> "StartupCondition":
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Automation:
|
class Automation:
|
||||||
"""Automation that activates a scene preset based on conditions."""
|
"""Automation that activates a scene preset based on conditions."""
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ class KeyColorsPictureTarget(PictureTarget):
|
|||||||
|
|
||||||
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
||||||
settings=None, key_colors_settings=None, description=None,
|
settings=None, key_colors_settings=None, description=None,
|
||||||
auto_start=None, **_kwargs) -> None:
|
**_kwargs) -> None:
|
||||||
"""Apply mutable field updates for KC targets."""
|
"""Apply mutable field updates for KC targets."""
|
||||||
super().update_fields(name=name, description=description, auto_start=auto_start)
|
super().update_fields(name=name, description=description)
|
||||||
if picture_source_id is not None:
|
if picture_source_id is not None:
|
||||||
self.picture_source_id = picture_source_id
|
self.picture_source_id = picture_source_id
|
||||||
if key_colors_settings is not None:
|
if key_colors_settings is not None:
|
||||||
@@ -130,7 +130,6 @@ class KeyColorsPictureTarget(PictureTarget):
|
|||||||
picture_source_id=data.get("picture_source_id", ""),
|
picture_source_id=data.get("picture_source_id", ""),
|
||||||
settings=settings,
|
settings=settings,
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
auto_start=data.get("auto_start", False),
|
|
||||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class PictureTarget:
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
auto_start: bool = False
|
|
||||||
|
|
||||||
def register_with_manager(self, manager) -> None:
|
def register_with_manager(self, manager) -> None:
|
||||||
"""Register this target with the processor manager. Subclasses override."""
|
"""Register this target with the processor manager. Subclasses override."""
|
||||||
@@ -27,14 +26,12 @@ class PictureTarget:
|
|||||||
|
|
||||||
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
|
||||||
settings=None, key_colors_settings=None, description=None,
|
settings=None, key_colors_settings=None, description=None,
|
||||||
auto_start=None) -> None:
|
**_kwargs) -> None:
|
||||||
"""Apply mutable field updates. Base handles common fields; subclasses handle type-specific ones."""
|
"""Apply mutable field updates. Base handles common fields; subclasses handle type-specific ones."""
|
||||||
if name is not None:
|
if name is not None:
|
||||||
self.name = name
|
self.name = name
|
||||||
if description is not None:
|
if description is not None:
|
||||||
self.description = description
|
self.description = description
|
||||||
if auto_start is not None:
|
|
||||||
self.auto_start = auto_start
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_picture_source(self) -> bool:
|
def has_picture_source(self) -> bool:
|
||||||
@@ -48,7 +45,6 @@ class PictureTarget:
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"target_type": self.target_type,
|
"target_type": self.target_type,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"auto_start": self.auto_start,
|
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": self.updated_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ class PictureTargetStore:
|
|||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
picture_source_id: str = "",
|
picture_source_id: str = "",
|
||||||
auto_start: bool = False,
|
|
||||||
) -> PictureTarget:
|
) -> PictureTarget:
|
||||||
"""Create a new picture target.
|
"""Create a new picture target.
|
||||||
|
|
||||||
@@ -138,7 +137,6 @@ class PictureTargetStore:
|
|||||||
adaptive_fps=adaptive_fps,
|
adaptive_fps=adaptive_fps,
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
)
|
)
|
||||||
@@ -150,7 +148,6 @@ class PictureTargetStore:
|
|||||||
picture_source_id=picture_source_id,
|
picture_source_id=picture_source_id,
|
||||||
settings=key_colors_settings or KeyColorsSettings(),
|
settings=key_colors_settings or KeyColorsSettings(),
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
)
|
)
|
||||||
@@ -178,7 +175,6 @@ class PictureTargetStore:
|
|||||||
protocol: Optional[str] = None,
|
protocol: Optional[str] = None,
|
||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
auto_start: Optional[bool] = None,
|
|
||||||
) -> PictureTarget:
|
) -> PictureTarget:
|
||||||
"""Update a picture target.
|
"""Update a picture target.
|
||||||
|
|
||||||
@@ -209,7 +205,6 @@ class PictureTargetStore:
|
|||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
key_colors_settings=key_colors_settings,
|
key_colors_settings=key_colors_settings,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target.updated_at = datetime.utcnow()
|
target.updated_at = datetime.utcnow()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class TargetSnapshot:
|
|||||||
color_strip_source_id: str = ""
|
color_strip_source_id: str = ""
|
||||||
brightness_value_source_id: str = ""
|
brightness_value_source_id: str = ""
|
||||||
fps: int = 30
|
fps: int = 30
|
||||||
auto_start: bool = False
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -23,7 +22,6 @@ class TargetSnapshot:
|
|||||||
"color_strip_source_id": self.color_strip_source_id,
|
"color_strip_source_id": self.color_strip_source_id,
|
||||||
"brightness_value_source_id": self.brightness_value_source_id,
|
"brightness_value_source_id": self.brightness_value_source_id,
|
||||||
"fps": self.fps,
|
"fps": self.fps,
|
||||||
"auto_start": self.auto_start,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -34,7 +32,6 @@ class TargetSnapshot:
|
|||||||
color_strip_source_id=data.get("color_strip_source_id", ""),
|
color_strip_source_id=data.get("color_strip_source_id", ""),
|
||||||
brightness_value_source_id=data.get("brightness_value_source_id", ""),
|
brightness_value_source_id=data.get("brightness_value_source_id", ""),
|
||||||
fps=data.get("fps", 30),
|
fps=data.get("fps", 30),
|
||||||
auto_start=data.get("auto_start", False),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +42,6 @@ class ScenePreset:
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
color: str = "#4fc3f7" # accent color for the card
|
|
||||||
targets: List[TargetSnapshot] = field(default_factory=list)
|
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||||
order: int = 0
|
order: int = 0
|
||||||
created_at: datetime = field(default_factory=datetime.utcnow)
|
created_at: datetime = field(default_factory=datetime.utcnow)
|
||||||
@@ -56,7 +52,6 @@ class ScenePreset:
|
|||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"color": self.color,
|
|
||||||
"targets": [t.to_dict() for t in self.targets],
|
"targets": [t.to_dict() for t in self.targets],
|
||||||
"order": self.order,
|
"order": self.order,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
@@ -69,7 +64,6 @@ class ScenePreset:
|
|||||||
id=data["id"],
|
id=data["id"],
|
||||||
name=data["name"],
|
name=data["name"],
|
||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
color=data.get("color", "#4fc3f7"),
|
|
||||||
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||||
order=data.get("order", 0),
|
order=data.get("order", 0),
|
||||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ class ScenePresetStore:
|
|||||||
preset_id: str,
|
preset_id: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
color: Optional[str] = None,
|
|
||||||
order: Optional[int] = None,
|
order: Optional[int] = None,
|
||||||
) -> ScenePreset:
|
) -> ScenePreset:
|
||||||
if preset_id not in self._presets:
|
if preset_id not in self._presets:
|
||||||
@@ -98,8 +97,6 @@ class ScenePresetStore:
|
|||||||
preset.name = name
|
preset.name = name
|
||||||
if description is not None:
|
if description is not None:
|
||||||
preset.description = description
|
preset.description = description
|
||||||
if color is not None:
|
|
||||||
preset.color = color
|
|
||||||
if order is not None:
|
if order is not None:
|
||||||
preset.order = order
|
preset.order = order
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ class WledPictureTarget(PictureTarget):
|
|||||||
brightness_value_source_id=None,
|
brightness_value_source_id=None,
|
||||||
fps=None, keepalive_interval=None, state_check_interval=None,
|
fps=None, keepalive_interval=None, state_check_interval=None,
|
||||||
min_brightness_threshold=None, adaptive_fps=None, protocol=None,
|
min_brightness_threshold=None, adaptive_fps=None, protocol=None,
|
||||||
description=None, auto_start=None, **_kwargs) -> None:
|
description=None, **_kwargs) -> None:
|
||||||
"""Apply mutable field updates for WLED targets."""
|
"""Apply mutable field updates for WLED targets."""
|
||||||
super().update_fields(name=name, description=description, auto_start=auto_start)
|
super().update_fields(name=name, description=description)
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
if color_strip_source_id is not None:
|
if color_strip_source_id is not None:
|
||||||
@@ -120,7 +120,6 @@ class WledPictureTarget(PictureTarget):
|
|||||||
adaptive_fps=data.get("adaptive_fps", False),
|
adaptive_fps=data.get("adaptive_fps", False),
|
||||||
protocol=data.get("protocol", "ddp"),
|
protocol=data.get("protocol", "ddp"),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
auto_start=data.get("auto_start", False),
|
|
||||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,15 +27,6 @@
|
|||||||
<input type="text" id="scene-preset-editor-description">
|
<input type="text" id="scene-preset-editor-description">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="label-row">
|
|
||||||
<label for="scene-preset-editor-color" data-i18n="scenes.color">Card Color:</label>
|
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
|
||||||
</div>
|
|
||||||
<small class="input-hint" style="display:none" data-i18n="scenes.color.hint">Accent color for the scene card on the dashboard</small>
|
|
||||||
<input type="color" id="scene-preset-editor-color" value="#4fc3f7">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="scene-target-selector-group" style="display:none">
|
<div class="form-group" id="scene-target-selector-group" style="display:none">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label data-i18n="scenes.targets">Targets:</label>
|
<label data-i18n="scenes.targets">Targets:</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user