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:
2026-02-28 19:35:20 +03:00
parent ff4e7f8adb
commit a34edf9650
11 changed files with 220 additions and 176 deletions

View File

@@ -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);