Simplify scenes to capture only target state, add target selector
- Remove DeviceBrightnessSnapshot and AutomationSnapshot from scene data model - Simplify capture_current_snapshot and apply_scene_state to targets only - Remove device/automation dependencies from scene preset API routes - Add target selector (combobox + add/remove) to scene capture modal - Fix stale profiles reference bug in scene_preset_store recapture - Update automation engine call sites for simplified scene functions - Sync scene presets cache between automations and scene-presets modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -413,3 +413,27 @@ textarea:focus-visible {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* Scene target selector */
|
||||
.scene-target-add-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.scene-target-add-row select { flex: 1; }
|
||||
.scene-target-add-row .btn { padding: 4px 10px; min-width: 0; flex: 0 0 auto; font-size: 0.85rem; }
|
||||
.scene-target-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.scene-target-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
import {
|
||||
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
|
||||
activateScenePreset, recaptureScenePreset, deleteScenePreset,
|
||||
addSceneTarget, removeSceneTarget,
|
||||
} from './features/scene-presets.js';
|
||||
|
||||
// Layer 5: device-discovery, targets
|
||||
@@ -318,6 +319,8 @@ Object.assign(window, {
|
||||
activateScenePreset,
|
||||
recaptureScenePreset,
|
||||
deleteScenePreset,
|
||||
addSceneTarget,
|
||||
removeSceneTarget,
|
||||
|
||||
// device-discovery
|
||||
onDeviceTypeChanged,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
import { csScenes, createSceneCard, updatePresetsCache } from './scene-presets.js';
|
||||
|
||||
// ===== Scene presets cache (shared by both selectors) =====
|
||||
let _scenesCache = [];
|
||||
@@ -62,6 +62,7 @@ export async function loadAutomations() {
|
||||
const data = await automationsResp.json();
|
||||
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
||||
_scenesCache = scenesData.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
|
||||
// Build scene name map for card rendering
|
||||
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
||||
@@ -208,6 +209,7 @@ export async function openAutomationEditor(automationId) {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_scenesCache = data.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
}
|
||||
} catch { /* use cached */ }
|
||||
|
||||
|
||||
@@ -9,19 +9,26 @@ import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import {
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_SETTINGS,
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||
} from '../core/icons.js';
|
||||
|
||||
let _presetsCache = [];
|
||||
let _editingId = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
|
||||
/** Update the internal presets cache (called from automations tab after fetching). */
|
||||
export function updatePresetsCache(presets) { _presetsCache = presets; }
|
||||
|
||||
class ScenePresetEditorModal extends Modal {
|
||||
constructor() { super('scene-preset-editor-modal'); }
|
||||
snapshotValues() {
|
||||
const items = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId).sort().join(',');
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,14 +43,10 @@ export const csScenes = new CardSection('scenes', {
|
||||
|
||||
export function createSceneCard(preset) {
|
||||
const targetCount = (preset.targets || []).length;
|
||||
const deviceCount = (preset.devices || []).length;
|
||||
const automationCount = (preset.automations || []).length;
|
||||
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
||||
|
||||
const meta = [
|
||||
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
deviceCount > 0 ? `${ICON_SETTINGS} ${deviceCount} ${t('scenes.devices_count')}` : null,
|
||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
|
||||
@@ -94,13 +97,9 @@ export function renderScenePresetsSection(presets) {
|
||||
function _renderDashboardPresetCard(preset) {
|
||||
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
||||
const targetCount = (preset.targets || []).length;
|
||||
const deviceCount = (preset.devices || []).length;
|
||||
const automationCount = (preset.automations || []).length;
|
||||
|
||||
const subtitle = [
|
||||
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
deviceCount > 0 ? `${deviceCount} ${t('scenes.devices_count')}` : null,
|
||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_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}')}">
|
||||
@@ -120,7 +119,7 @@ function _renderDashboardPresetCard(preset) {
|
||||
|
||||
// ===== Capture (create) =====
|
||||
|
||||
export function openScenePresetCapture() {
|
||||
export async function openScenePresetCapture() {
|
||||
_editingId = null;
|
||||
document.getElementById('scene-preset-editor-id').value = '';
|
||||
document.getElementById('scene-preset-editor-name').value = '';
|
||||
@@ -131,6 +130,22 @@ export function openScenePresetCapture() {
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.add'); titleEl.textContent = t('scenes.add'); }
|
||||
|
||||
// Fetch targets and populate selector
|
||||
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||
const targetList = document.getElementById('scene-target-list');
|
||||
if (selectorGroup && targetList) {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-targets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_allTargets = data.targets || [];
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
scenePresetModal.open();
|
||||
scenePresetModal.snapshot();
|
||||
}
|
||||
@@ -148,6 +163,10 @@ export async function editScenePreset(presetId) {
|
||||
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)
|
||||
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||
if (selectorGroup) selectorGroup.style.display = 'none';
|
||||
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.edit'); titleEl.textContent = t('scenes.edit'); }
|
||||
|
||||
@@ -177,9 +196,11 @@ export async function saveScenePreset() {
|
||||
body: JSON.stringify({ name, description, color }),
|
||||
});
|
||||
} 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 }),
|
||||
body: JSON.stringify({ name, description, color, target_ids }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,6 +225,49 @@ export async function closeScenePresetEditor() {
|
||||
await scenePresetModal.close();
|
||||
}
|
||||
|
||||
// ===== Target selector helpers =====
|
||||
|
||||
function _refreshTargetSelect() {
|
||||
const select = document.getElementById('scene-target-select');
|
||||
if (!select) return;
|
||||
const added = new Set(
|
||||
[...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId)
|
||||
);
|
||||
select.innerHTML = '';
|
||||
for (const tgt of _allTargets) {
|
||||
if (added.has(tgt.id)) continue;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tgt.id;
|
||||
opt.textContent = tgt.name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
// Disable add button when no targets available
|
||||
const addBtn = select.parentElement?.querySelector('button');
|
||||
if (addBtn) addBtn.disabled = select.options.length === 0;
|
||||
}
|
||||
|
||||
export function addSceneTarget() {
|
||||
const select = document.getElementById('scene-target-select');
|
||||
const list = document.getElementById('scene-target-list');
|
||||
if (!select || !list || !select.value) return;
|
||||
|
||||
const targetId = select.value;
|
||||
const targetName = select.options[select.selectedIndex].text;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = targetId;
|
||||
item.innerHTML = `<span>${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
list.appendChild(item);
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
export function removeSceneTarget(btn) {
|
||||
btn.closest('.scene-target-item').remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
// ===== Activate =====
|
||||
|
||||
export async function activateScenePreset(presetId) {
|
||||
|
||||
@@ -630,13 +630,13 @@
|
||||
"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",
|
||||
"scenes.activate": "Activate scene",
|
||||
"scenes.recapture": "Recapture current state",
|
||||
"scenes.delete": "Delete scene",
|
||||
"scenes.targets_count": "targets",
|
||||
"scenes.devices_count": "devices",
|
||||
"scenes.automations_count": "automations",
|
||||
"scenes.captured": "Scene captured",
|
||||
"scenes.updated": "Scene updated",
|
||||
"scenes.activated": "Scene activated",
|
||||
|
||||
@@ -630,13 +630,13 @@
|
||||
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
||||
"scenes.color": "Цвет карточки:",
|
||||
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
|
||||
"scenes.targets": "Цели:",
|
||||
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
|
||||
"scenes.capture": "Захват",
|
||||
"scenes.activate": "Активировать сцену",
|
||||
"scenes.recapture": "Перезахватить текущее состояние",
|
||||
"scenes.delete": "Удалить сцену",
|
||||
"scenes.targets_count": "целей",
|
||||
"scenes.devices_count": "устройств",
|
||||
"scenes.automations_count": "автоматизаций",
|
||||
"scenes.captured": "Сцена захвачена",
|
||||
"scenes.updated": "Сцена обновлена",
|
||||
"scenes.activated": "Сцена активирована",
|
||||
|
||||
@@ -630,13 +630,13 @@
|
||||
"scenes.description.hint": "此场景功能的可选描述",
|
||||
"scenes.color": "卡片颜色:",
|
||||
"scenes.color.hint": "仪表盘上场景卡片的强调色",
|
||||
"scenes.targets": "目标:",
|
||||
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
|
||||
"scenes.capture": "捕获",
|
||||
"scenes.activate": "激活场景",
|
||||
"scenes.recapture": "重新捕获当前状态",
|
||||
"scenes.delete": "删除场景",
|
||||
"scenes.targets_count": "目标",
|
||||
"scenes.devices_count": "设备",
|
||||
"scenes.automations_count": "自动化",
|
||||
"scenes.captured": "场景已捕获",
|
||||
"scenes.updated": "场景已更新",
|
||||
"scenes.activated": "场景已激活",
|
||||
|
||||
Reference in New Issue
Block a user