Add reusable DataCache class, unify frontend cache patterns
- Create DataCache class with fetch deduplication, invalidation, subscribers - Instantiate 10 cache instances in state.js (streams, templates, sources, etc.) - Replace inline fetch+parse+set patterns with cache.fetch() calls across modules - Eliminate dual _scenesCache/_presetsCache sync via shared scenePresetsCache - Remove 9 now-unused setter functions from state.js - Clean up unused setter imports from audio-sources, value-sources, displays Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Automations — automation cards, editor, condition builder, process picker, scene selector.
|
||||
*/
|
||||
|
||||
import { apiKey, _automationsCache, set_automationsCache, _automationsLoading, set_automationsLoading } from '../core/state.js';
|
||||
import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache } from '../core/state.js';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
|
||||
@@ -10,10 +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, updatePresetsCache } from './scene-presets.js';
|
||||
|
||||
// ===== Scene presets cache (shared by both selectors) =====
|
||||
let _scenesCache = [];
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
|
||||
class AutomationEditorModal extends Modal {
|
||||
constructor() { super('automation-editor-modal'); }
|
||||
@@ -54,23 +51,15 @@ export async function loadAutomations() {
|
||||
setTabRefreshing('automations-content', true);
|
||||
|
||||
try {
|
||||
const [automationsResp, scenesResp] = await Promise.all([
|
||||
fetchWithAuth('/automations'),
|
||||
fetchWithAuth('/scene-presets'),
|
||||
const [automations, scenes] = await Promise.all([
|
||||
automationsCacheObj.fetch(),
|
||||
scenePresetsCache.fetch(),
|
||||
]);
|
||||
if (!automationsResp.ok) throw new Error('Failed to load automations');
|
||||
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]));
|
||||
|
||||
set_automationsCache(data.automations);
|
||||
const activeCount = data.automations.filter(a => a.is_active).length;
|
||||
const sceneMap = new Map(scenes.map(s => [s.id, s]));
|
||||
const activeCount = automations.filter(a => a.is_active).length;
|
||||
updateTabBadge('automations', activeCount);
|
||||
renderAutomations(data.automations, sceneMap);
|
||||
renderAutomations(automations, sceneMap);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load automations:', error);
|
||||
@@ -93,7 +82,7 @@ function renderAutomations(automations, sceneMap) {
|
||||
const container = document.getElementById('automations-content');
|
||||
|
||||
const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
|
||||
const sceneItems = csScenes.applySortOrder(_scenesCache.map(s => ({ key: s.id, html: createSceneCard(s) })));
|
||||
const sceneItems = csScenes.applySortOrder(scenePresetsCache.data.map(s => ({ key: s.id, html: createSceneCard(s) })));
|
||||
|
||||
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
container.innerHTML = toolbar + csAutomations.render(autoItems) + csScenes.render(sceneItems);
|
||||
@@ -205,12 +194,7 @@ export async function openAutomationEditor(automationId) {
|
||||
|
||||
// Fetch scenes for selector
|
||||
try {
|
||||
const resp = await fetchWithAuth('/scene-presets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_scenesCache = data.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
}
|
||||
await scenePresetsCache.fetch();
|
||||
} catch { /* use cached */ }
|
||||
|
||||
// Reset deactivation mode
|
||||
@@ -288,7 +272,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
|
||||
// Set initial display text
|
||||
if (selectedId) {
|
||||
const scene = _scenesCache.find(s => s.id === selectedId);
|
||||
const scene = scenePresetsCache.data.find(s => s.id === selectedId);
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
} else {
|
||||
@@ -299,7 +283,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
// Render dropdown items
|
||||
function renderDropdown(filter) {
|
||||
const query = (filter || '').toLowerCase();
|
||||
const filtered = query ? _scenesCache.filter(s => s.name.toLowerCase().includes(query)) : _scenesCache;
|
||||
const filtered = query ? scenePresetsCache.data.filter(s => s.name.toLowerCase().includes(query)) : scenePresetsCache.data;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dropdown.innerHTML = `<div class="scene-selector-empty">${t('automations.scene.none_available')}</div>`;
|
||||
@@ -314,7 +298,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
dropdown.querySelectorAll('.scene-selector-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.sceneId;
|
||||
const scene = _scenesCache.find(s => s.id === id);
|
||||
const scene = scenePresetsCache.data.find(s => s.id === id);
|
||||
hiddenInput.value = id;
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
@@ -333,7 +317,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
// If text doesn't match any scene, clear the hidden input
|
||||
const exactMatch = _scenesCache.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
const exactMatch = scenePresetsCache.data.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
if (!exactMatch) {
|
||||
hiddenInput.value = '';
|
||||
clearBtn.classList.toggle('visible', !!searchInput.value);
|
||||
|
||||
Reference in New Issue
Block a user