/** * Scene Presets — capture, activate, edit, delete system state snapshots. * Rendered as a CardSection inside the Automations tab, plus dashboard compact cards. */ import { fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; 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_CLONE, } from '../core/icons.js'; import { scenePresetsCache, outputTargetsCache, automationsCacheObj } from '../core/state.js'; import { TagInput, renderTagChips } from '../core/tag-input.js'; import { cardColorStyle, cardColorButton } from '../core/card-colors.js'; import { EntityPalette } from '../core/entity-palette.js'; let _editingId = null; let _allTargets = []; // fetched on capture open let _sceneTagsInput = null; class ScenePresetEditorModal extends Modal { constructor() { super('scene-preset-editor-modal'); } onForceClose() { if (_sceneTagsInput) { _sceneTagsInput.destroy(); _sceneTagsInput = null; } } 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, targets: items, tags: JSON.stringify(_sceneTagsInput ? _sceneTagsInput.getValue() : []), }; } } const scenePresetModal = new ScenePresetEditorModal(); export const csScenes = new CardSection('scenes', { titleKey: 'scenes.title', gridClass: 'devices-grid', addCardOnclick: "openScenePresetCapture()", keyAttr: 'data-scene-id', emptyKey: 'section.empty.scenes', }); export function createSceneCard(preset) { const targetCount = (preset.targets || []).length; const automations = automationsCacheObj.data || []; const usedByCount = automations.filter(a => a.scene_preset_id === preset.id).length; const meta = [ targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null, usedByCount > 0 ? `🔗 ${t('scene_preset.used_by').replace('%d', usedByCount)}` : null, ].filter(Boolean); const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : ''; const colorStyle = cardColorStyle(preset.id); return `
${escapeHtml(preset.name)}
${preset.description ? `
${escapeHtml(preset.description)}
` : ''}
${meta.map(m => `${m}`).join('')} ${updated ? `${updated}` : ''}
${renderTagChips(preset.tags)}
${cardColorButton(preset.id, 'data-scene-id')}
`; } // ===== Dashboard section (compact cards) ===== export async function loadScenePresets() { return scenePresetsCache.fetch(); } export function renderScenePresetsSection(presets) { if (!presets || presets.length === 0) return ''; const captureBtn = ``; const cards = presets.map(p => _renderDashboardPresetCard(p)).join(''); return { headerExtra: captureBtn, content: `
${cards}
` }; } function _renderDashboardPresetCard(preset) { const targetCount = (preset.targets || []).length; const subtitle = [ targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null, ].filter(Boolean).join(' \u00b7 '); const pStyle = cardColorStyle(preset.id); return ``; } // ===== Capture (create) ===== export async function openScenePresetCapture() { _editingId = null; document.getElementById('scene-preset-editor-id').value = ''; document.getElementById('scene-preset-editor-name').value = ''; document.getElementById('scene-preset-editor-description').value = ''; document.getElementById('scene-preset-editor-error').style.display = 'none'; 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 { _allTargets = await outputTargetsCache.fetch().catch(() => []); _refreshTargetSelect(); } catch { /* ignore */ } } if (_sceneTagsInput) { _sceneTagsInput.destroy(); _sceneTagsInput = null; } _sceneTagsInput = new TagInput(document.getElementById('scene-tags-container'), { placeholder: t('tags.placeholder') }); _sceneTagsInput.setValue([]); scenePresetModal.open(); scenePresetModal.snapshot(); } // ===== Edit metadata ===== export async function editScenePreset(presetId) { const preset = scenePresetsCache.data.find(p => p.id === presetId); if (!preset) return; _editingId = presetId; document.getElementById('scene-preset-editor-id').value = presetId; document.getElementById('scene-preset-editor-name').value = preset.name; document.getElementById('scene-preset-editor-description').value = preset.description || ''; document.getElementById('scene-preset-editor-error').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'); } // Show target selector and pre-populate with existing targets const selectorGroup = document.getElementById('scene-target-selector-group'); const targetList = document.getElementById('scene-target-list'); if (selectorGroup && targetList) { selectorGroup.style.display = ''; targetList.innerHTML = ''; try { _allTargets = await outputTargetsCache.fetch().catch(() => []); // Pre-add targets already in the preset const presetTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id); for (const tid of presetTargetIds) { const tgt = _allTargets.find(t => t.id === tid); if (!tgt) continue; const item = document.createElement('div'); item.className = 'scene-target-item'; item.dataset.targetId = tid; item.innerHTML = `${escapeHtml(tgt.name)}`; targetList.appendChild(item); } _refreshTargetSelect(); } catch { /* ignore */ } } if (_sceneTagsInput) { _sceneTagsInput.destroy(); _sceneTagsInput = null; } _sceneTagsInput = new TagInput(document.getElementById('scene-tags-container'), { placeholder: t('tags.placeholder') }); _sceneTagsInput.setValue(preset.tags || []); scenePresetModal.open(); scenePresetModal.snapshot(); } // ===== Save (create or update) ===== export async function saveScenePreset() { const name = document.getElementById('scene-preset-editor-name').value.trim(); const description = document.getElementById('scene-preset-editor-description').value.trim(); const errorEl = document.getElementById('scene-preset-editor-error'); if (!name) { errorEl.textContent = t('scenes.error.name_required'); errorEl.style.display = 'block'; return; } const tags = _sceneTagsInput ? _sceneTagsInput.getValue() : []; try { let resp; if (_editingId) { const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')] .map(el => el.dataset.targetId); resp = await fetchWithAuth(`/scene-presets/${_editingId}`, { method: 'PUT', body: JSON.stringify({ name, description, target_ids, tags }), }); } 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, target_ids, tags }), }); } if (!resp.ok) { const err = await resp.json(); errorEl.textContent = err.detail || t('scenes.error.save_failed'); errorEl.style.display = 'block'; return; } scenePresetModal.forceClose(); showToast(_editingId ? t('scenes.updated') : t('scenes.captured'), 'success'); _reloadScenesTab(); } catch (error) { if (error.isAuth) return; errorEl.textContent = t('scenes.error.save_failed'); errorEl.style.display = 'block'; } } export async function closeScenePresetEditor() { await scenePresetModal.close(); } // ===== Target selector helpers ===== function _getAddedTargetIds() { return new Set( [...document.querySelectorAll('#scene-target-list .scene-target-item')] .map(el => el.dataset.targetId) ); } function _refreshTargetSelect() { // Update add button disabled state const addBtn = document.getElementById('scene-target-add-btn'); if (addBtn) { const added = _getAddedTargetIds(); addBtn.disabled = _allTargets.every(t => added.has(t.id)); } } function _addTargetToList(targetId, targetName) { const list = document.getElementById('scene-target-list'); if (!list) return; const item = document.createElement('div'); item.className = 'scene-target-item'; item.dataset.targetId = targetId; item.innerHTML = `${ICON_TARGET} ${escapeHtml(targetName)}`; list.appendChild(item); _refreshTargetSelect(); } export async function addSceneTarget() { const added = _getAddedTargetIds(); const available = _allTargets.filter(t => !added.has(t.id)); if (available.length === 0) return; const items = available.map(t => ({ value: t.id, label: t.name, icon: ICON_TARGET, })); const picked = await EntityPalette.pick({ items, placeholder: t('scenes.targets.search_placeholder'), }); if (!picked) return; const tgt = _allTargets.find(t => t.id === picked); if (tgt) _addTargetToList(tgt.id, tgt.name); } export function removeSceneTarget(btn) { btn.closest('.scene-target-item').remove(); _refreshTargetSelect(); } // ===== Activate ===== export async function activateScenePreset(presetId) { try { const resp = await fetchWithAuth(`/scene-presets/${presetId}/activate`, { method: 'POST', }); if (!resp.ok) { showToast(t('scenes.error.activate_failed'), 'error'); return; } const result = await resp.json(); if (result.status === 'activated') { showToast(t('scenes.activated'), 'success'); } else { showToast(`${t('scenes.activated_partial')}: ${result.errors.length} ${t('scenes.errors')}`, 'warning'); } if (typeof window.loadDashboard === 'function') window.loadDashboard(true); } catch (error) { if (error.isAuth) return; showToast(t('scenes.error.activate_failed'), 'error'); } } // ===== Recapture ===== export async function recaptureScenePreset(presetId) { const preset = scenePresetsCache.data.find(p => p.id === presetId); const name = preset ? preset.name : presetId; const confirmed = await showConfirm(t('scenes.recapture_confirm', { name })); if (!confirmed) return; try { const resp = await fetchWithAuth(`/scene-presets/${presetId}/recapture`, { method: 'POST', }); if (resp.ok) { showToast(t('scenes.recaptured'), 'success'); _reloadScenesTab(); } else { showToast(t('scenes.error.recapture_failed'), 'error'); } } catch (error) { if (error.isAuth) return; showToast(t('scenes.error.recapture_failed'), 'error'); } } // ===== Clone ===== export async function cloneScenePreset(presetId) { const preset = scenePresetsCache.data.find(p => p.id === presetId); if (!preset) return; // Open the capture modal in create mode, prefilled from the cloned preset _editingId = null; document.getElementById('scene-preset-editor-id').value = ''; document.getElementById('scene-preset-editor-name').value = (preset.name || '') + ' (Copy)'; document.getElementById('scene-preset-editor-description').value = preset.description || ''; document.getElementById('scene-preset-editor-error').style.display = 'none'; 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, then pre-add the cloned preset's targets const selectorGroup = document.getElementById('scene-target-selector-group'); const targetList = document.getElementById('scene-target-list'); if (selectorGroup && targetList) { selectorGroup.style.display = ''; targetList.innerHTML = ''; try { _allTargets = await outputTargetsCache.fetch().catch(() => []); // Pre-add targets from the cloned preset const clonedTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id); for (const tid of clonedTargetIds) { const tgt = _allTargets.find(t => t.id === tid); if (!tgt) continue; const item = document.createElement('div'); item.className = 'scene-target-item'; item.dataset.targetId = tid; item.innerHTML = `${escapeHtml(tgt.name)}`; targetList.appendChild(item); } _refreshTargetSelect(); } catch { /* ignore */ } } if (_sceneTagsInput) { _sceneTagsInput.destroy(); _sceneTagsInput = null; } _sceneTagsInput = new TagInput(document.getElementById('scene-tags-container'), { placeholder: t('tags.placeholder') }); _sceneTagsInput.setValue(preset.tags || []); scenePresetModal.open(); scenePresetModal.snapshot(); } // ===== Delete ===== export async function deleteScenePreset(presetId) { const preset = scenePresetsCache.data.find(p => p.id === presetId); const name = preset ? preset.name : presetId; const confirmed = await showConfirm(t('scenes.delete_confirm', { name })); if (!confirmed) return; try { const resp = await fetchWithAuth(`/scene-presets/${presetId}`, { method: 'DELETE', }); if (resp.ok) { showToast(t('scenes.deleted'), 'success'); _reloadScenesTab(); } else { showToast(t('scenes.error.delete_failed'), 'error'); } } catch (error) { if (error.isAuth) return; showToast(t('scenes.error.delete_failed'), 'error'); } } // ===== Helpers ===== function _reloadScenesTab() { // Reload automations tab (which includes scenes section) if ((localStorage.getItem('activeTab') || 'dashboard') === 'automations') { if (typeof window.loadAutomations === 'function') window.loadAutomations(); } // Also refresh dashboard (scene presets section) if (typeof window.loadDashboard === 'function') window.loadDashboard(true); }