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:
2026-02-28 18:55:11 +03:00
parent 0eb0f44ddb
commit ff4e7f8adb
14 changed files with 157 additions and 204 deletions

View File

@@ -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 */ }

View File

@@ -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">&#x2715;</button>`;
list.appendChild(item);
_refreshTargetSelect();
}
export function removeSceneTarget(btn) {
btn.closest('.scene-target-item').remove();
_refreshTargetSelect();
}
// ===== Activate =====
export async function activateScenePreset(presetId) {