/**
* 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,
} from '../core/icons.js';
import { scenePresetsCache } from '../core/state.js';
import { cardColorStyle, cardColorButton } from '../core/card-colors.js';
let _editingId = null;
let _allTargets = []; // fetched on capture open
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,
targets: items,
};
}
}
const scenePresetModal = new ScenePresetEditorModal();
export const csScenes = new CardSection('scenes', {
titleKey: 'scenes.title',
gridClass: 'devices-grid',
addCardOnclick: "openScenePresetCapture()",
keyAttr: 'data-scene-id',
});
export function createSceneCard(preset) {
const targetCount = (preset.targets || []).length;
const meta = [
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
].filter(Boolean);
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
const colorStyle = cardColorStyle(preset.id);
return `
${preset.description ? `
${escapeHtml(preset.description)}
` : ''}
${meta.map(m => `${m}`).join('')}
${updated ? `${updated}` : ''}
${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 `
${ICON_SCENE}
${escapeHtml(preset.name)}
${preset.description ? `
${escapeHtml(preset.description)}
` : ''}
${subtitle}
`;
}
// ===== 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 {
const resp = await fetchWithAuth('/picture-targets');
if (resp.ok) {
const data = await resp.json();
_allTargets = data.targets || [];
_refreshTargetSelect();
}
} catch { /* ignore */ }
}
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';
// 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'); }
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;
}
try {
let resp;
if (_editingId) {
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
method: 'PUT',
body: JSON.stringify({ name, description }),
});
} else {
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
.map(el => el.dataset.targetId);
resp = await fetchWithAuth('/scene-presets', {
method: 'POST',
body: JSON.stringify({ name, description, target_ids }),
});
}
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 _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 = `${escapeHtml(targetName)}`;
list.appendChild(item);
_refreshTargetSelect();
}
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');
}
}
// ===== 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);
}