Migrate frontend from JavaScript to TypeScript
- Rename all 54 .js files to .ts, update esbuild entry point - Add tsconfig.json, TypeScript devDependency, typecheck script - Create types.ts with 25+ interfaces matching backend Pydantic schemas (Device, OutputTarget, ColorStripSource, PatternTemplate, ValueSource, AudioSource, PictureSource, ScenePreset, SyncClock, Automation, etc.) - Make DataCache generic (DataCache<T>) with typed state instances - Type all state variables in state.ts with proper entity types - Type all create*Card functions with proper entity interfaces - Type all function parameters and return types across all 54 files - Type core component constructors (CardSection, IconSelect, EntitySelect, FilterList, TagInput, TreeNav, Modal) with exported option interfaces - Add comprehensive global.d.ts for window function declarations - Type fetchWithAuth with FetchAuthOpts interface - Remove all (window as any) casts in favor of global.d.ts declarations - Zero tsc errors, esbuild bundle unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 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.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { showToast, showConfirm } from '../core/ui.ts';
|
||||
import { Modal } from '../core/modal.ts';
|
||||
import { CardSection } from '../core/card-sections.ts';
|
||||
import {
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_CLONE, ICON_TRASH,
|
||||
} from '../core/icons.ts';
|
||||
import { scenePresetsCache, outputTargetsCache, automationsCacheObj } from '../core/state.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { cardColorStyle, cardColorButton } from '../core/card-colors.ts';
|
||||
import { EntityPalette } from '../core/entity-palette.ts';
|
||||
import type { ScenePreset } from '../types.ts';
|
||||
|
||||
let _editingId: string | null = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
let _sceneTagsInput: TagInput | null = 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 as HTMLElement).dataset.targetId).sort().join(',');
|
||||
return {
|
||||
name: (document.getElementById('scene-preset-editor-name') as HTMLInputElement).value,
|
||||
description: (document.getElementById('scene-preset-editor-description') as HTMLInputElement).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',
|
||||
bulkActions: [{
|
||||
key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete',
|
||||
handler: async (ids) => {
|
||||
const results = await Promise.allSettled(ids.map(id =>
|
||||
fetchWithAuth(`/scene-presets/${id}`, { method: 'DELETE' })
|
||||
));
|
||||
const failed = results.filter(r => r.status === 'rejected' || (r.value && !r.value.ok)).length;
|
||||
if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning');
|
||||
else showToast(t('scenes.deleted'), 'success');
|
||||
scenePresetsCache.invalidate();
|
||||
if (window.loadAutomations) window.loadAutomations();
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
export function createSceneCard(preset: ScenePreset) {
|
||||
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 `<div class="card" data-scene-id="${preset.id}"${colorStyle ? ` style="${colorStyle}"` : ''}>
|
||||
<div class="card-top-actions">
|
||||
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">✕</button>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<div class="card-title" title="${escapeHtml(preset.name)}">${escapeHtml(preset.name)}</div>
|
||||
</div>
|
||||
${preset.description ? `<div class="card-subtitle"><span class="card-meta">${escapeHtml(preset.description)}</span></div>` : ''}
|
||||
<div class="stream-card-props">
|
||||
${meta.map(m => `<span class="stream-card-prop">${m}</span>`).join('')}
|
||||
${updated ? `<span class="stream-card-prop">${updated}</span>` : ''}
|
||||
</div>
|
||||
${renderTagChips(preset.tags)}
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneScenePreset('${preset.id}')" title="${t('common.clone')}">${ICON_CLONE}</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-success" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
${cardColorButton(preset.id, 'data-scene-id')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ===== Dashboard section (compact cards) =====
|
||||
|
||||
export async function loadScenePresets(): Promise<ScenePreset[]> {
|
||||
return scenePresetsCache.fetch();
|
||||
}
|
||||
|
||||
export function renderScenePresetsSection(presets: ScenePreset[]): string | { headerExtra: string; content: string } {
|
||||
if (!presets || presets.length === 0) return '';
|
||||
|
||||
const captureBtn = `<button class="btn btn-sm btn-primary dashboard-stop-all" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
|
||||
const cards = presets.map(p => _renderDashboardPresetCard(p)).join('');
|
||||
|
||||
return { headerExtra: captureBtn, content: `<div class="dashboard-autostart-grid">${cards}</div>` };
|
||||
}
|
||||
|
||||
function _renderDashboardPresetCard(preset: ScenePreset): string {
|
||||
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 `<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">
|
||||
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
||||
<div>
|
||||
<div class="dashboard-target-name">${escapeHtml(preset.name)}</div>
|
||||
${preset.description ? `<div class="dashboard-target-subtitle">${escapeHtml(preset.description)}</div>` : ''}
|
||||
<div class="dashboard-target-subtitle">${subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-action-btn start" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ===== Capture (create) =====
|
||||
|
||||
export async function openScenePresetCapture(): Promise<void> {
|
||||
_editingId = null;
|
||||
(document.getElementById('scene-preset-editor-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('scene-preset-editor-name') as HTMLInputElement).value = '';
|
||||
(document.getElementById('scene-preset-editor-description') as HTMLInputElement).value = '';
|
||||
|
||||
(document.getElementById('scene-preset-editor-error') as HTMLElement).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: string): Promise<void> {
|
||||
const preset = scenePresetsCache.data.find(p => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
_editingId = presetId;
|
||||
(document.getElementById('scene-preset-editor-id') as HTMLInputElement).value = presetId;
|
||||
(document.getElementById('scene-preset-editor-name') as HTMLInputElement).value = preset.name;
|
||||
(document.getElementById('scene-preset-editor-description') as HTMLInputElement).value = preset.description || '';
|
||||
(document.getElementById('scene-preset-editor-error') as HTMLElement).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 = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
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(): Promise<void> {
|
||||
const name = (document.getElementById('scene-preset-editor-name') as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById('scene-preset-editor-description') as HTMLInputElement).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 as HTMLElement).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 as HTMLElement).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');
|
||||
scenePresetsCache.invalidate();
|
||||
_reloadScenesTab();
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
errorEl.textContent = t('scenes.error.save_failed');
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeScenePresetEditor(): Promise<void> {
|
||||
await scenePresetModal.close();
|
||||
}
|
||||
|
||||
// ===== Target selector helpers =====
|
||||
|
||||
function _getAddedTargetIds(): Set<string> {
|
||||
return new Set(
|
||||
[...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => (el as HTMLElement).dataset.targetId)
|
||||
);
|
||||
}
|
||||
|
||||
function _refreshTargetSelect(): void {
|
||||
// Update add button disabled state
|
||||
const addBtn = document.getElementById('scene-target-add-btn') as HTMLButtonElement | null;
|
||||
if (addBtn) {
|
||||
const added = _getAddedTargetIds();
|
||||
addBtn.disabled = _allTargets.every(t => added.has(t.id));
|
||||
}
|
||||
}
|
||||
|
||||
function _addTargetToList(targetId: string, targetName: string): void {
|
||||
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 = `<span>${ICON_TARGET} ${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
list.appendChild(item);
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
export async function addSceneTarget(): Promise<void> {
|
||||
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: HTMLElement): void {
|
||||
btn.closest('.scene-target-item').remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
// ===== Activate =====
|
||||
|
||||
export async function activateScenePreset(presetId: string): Promise<void> {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/scene-presets/${presetId}/activate`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
const detail = errData.detail || errData.message || '';
|
||||
const detailStr = Array.isArray(detail) ? detail.map(d => d.msg || d).join('; ') : String(detail);
|
||||
showToast(detailStr || 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: any) {
|
||||
if (error.isAuth) return;
|
||||
showToast(t('scenes.error.activate_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Recapture =====
|
||||
|
||||
export async function recaptureScenePreset(presetId: string): Promise<void> {
|
||||
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');
|
||||
scenePresetsCache.invalidate();
|
||||
_reloadScenesTab();
|
||||
} else {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
const detail = errData.detail || errData.message || '';
|
||||
const detailStr = Array.isArray(detail) ? detail.map(d => d.msg || d).join('; ') : String(detail);
|
||||
showToast(detailStr || t('scenes.error.recapture_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(error.message || t('scenes.error.recapture_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Clone =====
|
||||
|
||||
export async function cloneScenePreset(presetId: string): Promise<void> {
|
||||
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') as HTMLInputElement).value = '';
|
||||
(document.getElementById('scene-preset-editor-name') as HTMLInputElement).value = (preset.name || '') + ' (Copy)';
|
||||
(document.getElementById('scene-preset-editor-description') as HTMLInputElement).value = preset.description || '';
|
||||
(document.getElementById('scene-preset-editor-error') as HTMLElement).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 = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
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: string): Promise<void> {
|
||||
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');
|
||||
scenePresetsCache.invalidate();
|
||||
_reloadScenesTab();
|
||||
} else {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
const detail = errData.detail || errData.message || '';
|
||||
const detailStr = Array.isArray(detail) ? detail.map(d => d.msg || d).join('; ') : String(detail);
|
||||
showToast(detailStr || t('scenes.error.delete_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(error.message || t('scenes.error.delete_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
function _reloadScenesTab(): void {
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user