diff --git a/server/src/wled_controller/static/js/core/api.js b/server/src/wled_controller/static/js/core/api.js index 6b8d752..9aad233 100644 --- a/server/src/wled_controller/static/js/core/api.js +++ b/server/src/wled_controller/static/js/core/api.js @@ -2,7 +2,7 @@ * API utilities — base URL, auth headers, fetch wrapper, helpers. */ -import { apiKey, setApiKey, refreshInterval, setRefreshInterval, set_cachedDisplays } from './state.js'; +import { apiKey, setApiKey, refreshInterval, setRefreshInterval, displaysCache } from './state.js'; export const API_BASE = '/api/v1'; @@ -127,19 +127,20 @@ export async function loadServerInfo() { } export async function loadDisplays(engineType = null) { - try { - const url = engineType - ? `/config/displays?engine_type=${engineType}` - : '/config/displays'; - const response = await fetchWithAuth(url); - const data = await response.json(); - - if (data.displays && data.displays.length > 0) { - set_cachedDisplays(data.displays); + if (engineType) { + // Filtered fetch — bypass cache (engine-specific display list) + try { + const response = await fetchWithAuth(`/config/displays?engine_type=${engineType}`); + const data = await response.json(); + if (data.displays && data.displays.length > 0) { + displaysCache.update(data.displays); + } + } catch (error) { + if (error instanceof ApiError && error.isAuth) return; + console.error('Failed to load displays:', error); } - } catch (error) { - if (error instanceof ApiError && error.isAuth) return; - console.error('Failed to load displays:', error); + } else { + await displaysCache.fetch(); } } diff --git a/server/src/wled_controller/static/js/core/cache.js b/server/src/wled_controller/static/js/core/cache.js new file mode 100644 index 0000000..feaddf6 --- /dev/null +++ b/server/src/wled_controller/static/js/core/cache.js @@ -0,0 +1,73 @@ +/** + * Reusable data cache with fetch deduplication, invalidation, and subscribers. + */ + +import { fetchWithAuth } from './api.js'; + +export class DataCache { + /** + * @param {Object} opts + * @param {string} opts.endpoint - API path (e.g. '/picture-sources') + * @param {function} opts.extractData - Extract array from response JSON + * @param {*} [opts.defaultValue=[]] - Initial/fallback value + */ + constructor({ endpoint, extractData, defaultValue = [] }) { + this._endpoint = endpoint; + this._extractData = extractData; + this._defaultValue = defaultValue; + this._data = defaultValue; + this._loading = false; + this._promise = null; + this._subscribers = []; + } + + get data() { return this._data; } + get loading() { return this._loading; } + + /** Fetch from API. Deduplicates concurrent calls. */ + async fetch() { + if (this._promise) return this._promise; + this._loading = true; + this._promise = this._doFetch(); + try { + return await this._promise; + } finally { + this._promise = null; + this._loading = false; + } + } + + async _doFetch() { + try { + const resp = await fetchWithAuth(this._endpoint); + if (!resp.ok) return this._data; + const json = await resp.json(); + this._data = this._extractData(json); + this._notify(); + return this._data; + } catch (err) { + if (err.isAuth) return this._data; + console.error(`Cache fetch ${this._endpoint}:`, err); + return this._data; + } + } + + /** Clear cached data; next fetch() will re-request. */ + invalidate() { + this._data = this._defaultValue; + this._notify(); + } + + /** Manually set cache value (e.g. after a create/update call). */ + update(value) { + this._data = value; + this._notify(); + } + + subscribe(fn) { this._subscribers.push(fn); } + unsubscribe(fn) { this._subscribers = this._subscribers.filter(f => f !== fn); } + + _notify() { + for (const fn of this._subscribers) fn(this._data); + } +} diff --git a/server/src/wled_controller/static/js/core/state.js b/server/src/wled_controller/static/js/core/state.js index 0960d3e..906c0e4 100644 --- a/server/src/wled_controller/static/js/core/state.js +++ b/server/src/wled_controller/static/js/core/state.js @@ -6,6 +6,8 @@ * gets a setter function. */ +import { DataCache } from './cache.js'; + export let apiKey = null; export function setApiKey(v) { apiKey = v; } @@ -19,7 +21,6 @@ export let kcTestTargetId = null; export function setKcTestTargetId(v) { kcTestTargetId = v; } export let _cachedDisplays = null; -export function set_cachedDisplays(v) { _cachedDisplays = v; } export let _displayPickerCallback = null; export function set_displayPickerCallback(v) { _displayPickerCallback = v; } @@ -56,16 +57,9 @@ export function set_discoveryCache(v) { _discoveryCache = v; } // Streams / templates state export let _cachedStreams = []; -export function set_cachedStreams(v) { _cachedStreams = v; } - export let _cachedPPTemplates = []; -export function set_cachedPPTemplates(v) { _cachedPPTemplates = v; } - export let _cachedCaptureTemplates = []; -export function set_cachedCaptureTemplates(v) { _cachedCaptureTemplates = v; } - export let _availableFilters = []; -export function set_availableFilters(v) { _availableFilters = v; } export let availableEngines = []; export function setAvailableEngines(v) { availableEngines = v; } @@ -176,11 +170,7 @@ export const PATTERN_RECT_BORDERS = [ // Audio sources export let _cachedAudioSources = []; -export function set_cachedAudioSources(v) { _cachedAudioSources = v; } - -// Audio templates export let _cachedAudioTemplates = []; -export function set_cachedAudioTemplates(v) { _cachedAudioTemplates = v; } export let availableAudioEngines = []; export function setAvailableAudioEngines(v) { availableAudioEngines = v; } @@ -193,8 +183,70 @@ export function set_audioTemplateNameManuallyEdited(v) { _audioTemplateNameManua // Value sources export let _cachedValueSources = []; -export function set_cachedValueSources(v) { _cachedValueSources = v; } // Automations export let _automationsCache = null; -export function set_automationsCache(v) { _automationsCache = v; } + +// ─── DataCache instances ─────────────────────────────────────────── +// Each cache syncs its data into the existing `export let` variable +// via a subscriber, preserving backward compatibility. + +export const displaysCache = new DataCache({ + endpoint: '/config/displays', + extractData: json => json.displays || [], + defaultValue: null, +}); +displaysCache.subscribe(v => { _cachedDisplays = v; }); + +export const streamsCache = new DataCache({ + endpoint: '/picture-sources', + extractData: json => json.streams || [], +}); +streamsCache.subscribe(v => { _cachedStreams = v; }); + +export const ppTemplatesCache = new DataCache({ + endpoint: '/postprocessing-templates', + extractData: json => json.templates || [], +}); +ppTemplatesCache.subscribe(v => { _cachedPPTemplates = v; }); + +export const captureTemplatesCache = new DataCache({ + endpoint: '/capture-templates', + extractData: json => json.templates || [], +}); +captureTemplatesCache.subscribe(v => { _cachedCaptureTemplates = v; }); + +export const audioSourcesCache = new DataCache({ + endpoint: '/audio-sources', + extractData: json => json.sources || [], +}); +audioSourcesCache.subscribe(v => { _cachedAudioSources = v; }); + +export const audioTemplatesCache = new DataCache({ + endpoint: '/audio-templates', + extractData: json => json.templates || [], +}); +audioTemplatesCache.subscribe(v => { _cachedAudioTemplates = v; }); + +export const valueSourcesCache = new DataCache({ + endpoint: '/value-sources', + extractData: json => json.sources || [], +}); +valueSourcesCache.subscribe(v => { _cachedValueSources = v; }); + +export const filtersCache = new DataCache({ + endpoint: '/filters', + extractData: json => json.filters || [], +}); +filtersCache.subscribe(v => { _availableFilters = v; }); + +export const automationsCacheObj = new DataCache({ + endpoint: '/automations', + extractData: json => json.automations || [], +}); +automationsCacheObj.subscribe(v => { _automationsCache = v; }); + +export const scenePresetsCache = new DataCache({ + endpoint: '/scene-presets', + extractData: json => json.presets || [], +}); diff --git a/server/src/wled_controller/static/js/features/audio-sources.js b/server/src/wled_controller/static/js/features/audio-sources.js index 0d77362..1b53753 100644 --- a/server/src/wled_controller/static/js/features/audio-sources.js +++ b/server/src/wled_controller/static/js/features/audio-sources.js @@ -10,7 +10,7 @@ * This module manages the editor modal and API operations. */ -import { _cachedAudioSources, set_cachedAudioSources, _cachedAudioTemplates, apiKey } from '../core/state.js'; +import { _cachedAudioSources, _cachedAudioTemplates, apiKey } from '../core/state.js'; import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm, lockBody, unlockBody } from '../core/ui.js'; diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js index 32d3197..7dd9bf2 100644 --- a/server/src/wled_controller/static/js/features/automations.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -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 = `
`; 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 = `
${t('automations.scene.none_available')}
`; @@ -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); diff --git a/server/src/wled_controller/static/js/features/displays.js b/server/src/wled_controller/static/js/features/displays.js index ccc28be..6268d69 100644 --- a/server/src/wled_controller/static/js/features/displays.js +++ b/server/src/wled_controller/static/js/features/displays.js @@ -5,10 +5,9 @@ import { _cachedDisplays, _displayPickerCallback, _displayPickerSelectedIndex, - set_displayPickerCallback, set_displayPickerSelectedIndex, set_cachedDisplays, + set_displayPickerCallback, set_displayPickerSelectedIndex, displaysCache, } from '../core/state.js'; import { t } from '../core/i18n.js'; -import { loadDisplays } from '../core/api.js'; import { fetchWithAuth } from '../core/api.js'; import { showToast } from '../core/ui.js'; @@ -32,14 +31,12 @@ export function openDisplayPicker(callback, selectedIndex, engineType = null) { } else { const canvas = document.getElementById('display-picker-canvas'); canvas.innerHTML = '
'; - loadDisplays().then(() => { - import('../core/state.js').then(({ _cachedDisplays: displays }) => { - if (displays && displays.length > 0) { - renderDisplayPickerLayout(displays); - } else { - canvas.innerHTML = `
${t('displays.none')}
`; - } - }); + displaysCache.fetch().then(displays => { + if (displays && displays.length > 0) { + renderDisplayPickerLayout(displays); + } else { + canvas.innerHTML = `
${t('displays.none')}
`; + } }); } }); @@ -56,7 +53,7 @@ async function _fetchAndRenderEngineDisplays(engineType) { const displays = data.displays || []; // Store in cache so selectDisplay() can look them up - set_cachedDisplays(displays); + displaysCache.update(displays); if (displays.length > 0) { renderDisplayPickerLayout(displays, engineType); diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index e3900eb..86c6902 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -8,7 +8,7 @@ import { _kcNameManuallyEdited, set_kcNameManuallyEdited, kcWebSockets, PATTERN_RECT_BORDERS, - _cachedValueSources, set_cachedValueSources, + _cachedValueSources, valueSourcesCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -413,15 +413,13 @@ function _populateKCBrightnessVsDropdown(selectedId = '') { export async function showKCEditor(targetId = null, cloneData = null) { try { // Load sources, pattern templates, and value sources in parallel - const [sourcesResp, patResp, vsResp] = await Promise.all([ + const [sourcesResp, patResp, valueSources] = await Promise.all([ fetchWithAuth('/picture-sources').catch(() => null), fetchWithAuth('/pattern-templates').catch(() => null), - fetchWithAuth('/value-sources').catch(() => null), + valueSourcesCache.fetch(), ]); const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : []; const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : []; - const valueSources = (vsResp && vsResp.ok) ? (await vsResp.json()).sources || [] : []; - set_cachedValueSources(valueSources); // Populate source select const sourceSelect = document.getElementById('kc-editor-source'); diff --git a/server/src/wled_controller/static/js/features/scene-presets.js b/server/src/wled_controller/static/js/features/scene-presets.js index ddb1dd8..a87e826 100644 --- a/server/src/wled_controller/static/js/features/scene-presets.js +++ b/server/src/wled_controller/static/js/features/scene-presets.js @@ -11,14 +11,11 @@ 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'; -let _presetsCache = []; let _editingId = null; let _allTargets = []; // fetched on capture open -/** Update the internal presets cache (called from automations tab after fetching). */ -export function updatePresetsCache(presets) { _presetsCache = presets; } - class ScenePresetEditorModal extends Modal { constructor() { super('scene-preset-editor-modal'); } snapshotValues() { @@ -74,15 +71,7 @@ export function createSceneCard(preset) { // ===== Dashboard section (compact cards) ===== export async function loadScenePresets() { - try { - const resp = await fetchWithAuth('/scene-presets'); - if (!resp.ok) return []; - const data = await resp.json(); - _presetsCache = data.presets || []; - return _presetsCache; - } catch { - return []; - } + return scenePresetsCache.fetch(); } export function renderScenePresetsSection(presets) { @@ -153,7 +142,7 @@ export async function openScenePresetCapture() { // ===== Edit metadata ===== export async function editScenePreset(presetId) { - const preset = _presetsCache.find(p => p.id === presetId); + const preset = scenePresetsCache.data.find(p => p.id === presetId); if (!preset) return; _editingId = presetId; @@ -295,7 +284,7 @@ export async function activateScenePreset(presetId) { // ===== Recapture ===== export async function recaptureScenePreset(presetId) { - const preset = _presetsCache.find(p => p.id === 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; @@ -319,7 +308,7 @@ export async function recaptureScenePreset(presetId) { // ===== Delete ===== export async function deleteScenePreset(presetId) { - const preset = _presetsCache.find(p => p.id === 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; diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 8756ee5..4174e19 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -3,11 +3,12 @@ */ import { - _cachedDisplays, set_cachedDisplays, - _cachedStreams, set_cachedStreams, - _cachedPPTemplates, set_cachedPPTemplates, - _cachedCaptureTemplates, set_cachedCaptureTemplates, - _availableFilters, set_availableFilters, + _cachedDisplays, + displaysCache, + _cachedStreams, + _cachedPPTemplates, + _cachedCaptureTemplates, + _availableFilters, availableEngines, setAvailableEngines, currentEditingTemplateId, setCurrentEditingTemplateId, _templateNameManuallyEdited, set_templateNameManuallyEdited, @@ -18,14 +19,16 @@ import { _currentTestStreamId, set_currentTestStreamId, _currentTestPPTemplateId, set_currentTestPPTemplateId, _lastValidatedImageSource, set_lastValidatedImageSource, - _cachedAudioSources, set_cachedAudioSources, - _cachedValueSources, set_cachedValueSources, - _cachedAudioTemplates, set_cachedAudioTemplates, + _cachedAudioSources, + _cachedValueSources, + _cachedAudioTemplates, availableAudioEngines, setAvailableAudioEngines, currentEditingAudioTemplateId, setCurrentEditingAudioTemplateId, _audioTemplateNameManuallyEdited, set_audioTemplateNameManuallyEdited, _sourcesLoading, set_sourcesLoading, apiKey, + streamsCache, ppTemplatesCache, captureTemplatesCache, + audioSourcesCache, audioTemplatesCache, valueSourcesCache, filtersCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -153,10 +156,7 @@ const audioTemplateModal = new AudioTemplateModal(); async function loadCaptureTemplates() { try { - const response = await fetchWithAuth('/capture-templates'); - if (!response.ok) throw new Error(`Failed to load templates: ${response.status}`); - const data = await response.json(); - set_cachedCaptureTemplates(data.templates || []); + await captureTemplatesCache.fetch(); renderPictureSourcesList(_cachedStreams); } catch (error) { if (error.isAuth) return; @@ -397,7 +397,7 @@ async function loadDisplaysForTest() { const response = await fetchWithAuth(url); if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`); const displaysData = await response.json(); - set_cachedDisplays(displaysData.displays || []); + displaysCache.update(displaysData.displays || []); } let selectedIndex = null; @@ -745,10 +745,7 @@ function collectAudioEngineConfig() { async function loadAudioTemplates() { try { - const response = await fetchWithAuth('/audio-templates'); - if (!response.ok) throw new Error(`Failed to load audio templates: ${response.status}`); - const data = await response.json(); - set_cachedAudioTemplates(data.templates || []); + await audioTemplatesCache.fetch(); renderPictureSourcesList(_cachedStreams); } catch (error) { if (error.isAuth) return; @@ -1096,44 +1093,16 @@ export async function loadPictureSources() { set_sourcesLoading(true); setTabRefreshing('streams-list', true); try { - const [filtersResp, ppResp, captResp, streamsResp, audioResp, valueResp, audioTplResp] = await Promise.all([ - _availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null), - fetchWithAuth('/postprocessing-templates'), - fetchWithAuth('/capture-templates'), - fetchWithAuth('/picture-sources'), - fetchWithAuth('/audio-sources'), - fetchWithAuth('/value-sources'), - fetchWithAuth('/audio-templates'), + const [streams] = await Promise.all([ + streamsCache.fetch(), + ppTemplatesCache.fetch(), + captureTemplatesCache.fetch(), + audioSourcesCache.fetch(), + valueSourcesCache.fetch(), + audioTemplatesCache.fetch(), + filtersCache.data.length === 0 ? filtersCache.fetch() : Promise.resolve(filtersCache.data), ]); - - if (filtersResp && filtersResp.ok) { - const fd = await filtersResp.json(); - set_availableFilters(fd.filters || []); - } - if (ppResp.ok) { - const pd = await ppResp.json(); - set_cachedPPTemplates(pd.templates || []); - } - if (captResp.ok) { - const cd = await captResp.json(); - set_cachedCaptureTemplates(cd.templates || []); - } - if (audioResp && audioResp.ok) { - const ad = await audioResp.json(); - set_cachedAudioSources(ad.sources || []); - } - if (valueResp && valueResp.ok) { - const vd = await valueResp.json(); - set_cachedValueSources(vd.sources || []); - } - if (audioTplResp && audioTplResp.ok) { - const atd = await audioTplResp.json(); - set_cachedAudioTemplates(atd.templates || []); - } - if (!streamsResp.ok) throw new Error(`Failed to load streams: ${streamsResp.status}`); - const data = await streamsResp.json(); - set_cachedStreams(data.streams || []); - renderPictureSourcesList(_cachedStreams); + renderPictureSourcesList(streams); } catch (error) { if (error.isAuth) return; console.error('Error loading picture sources:', error); @@ -1576,7 +1545,7 @@ async function populateStreamModalDropdowns() { if (displaysRes.ok) { const displaysData = await displaysRes.json(); - set_cachedDisplays(displaysData.displays || []); + displaysCache.update(displaysData.displays || []); } _streamModalDisplaysEngine = null; // desktop displays loaded @@ -1660,7 +1629,7 @@ async function _refreshStreamDisplaysForEngine(engineType) { const resp = await fetchWithAuth(url); if (resp.ok) { const data = await resp.json(); - set_cachedDisplays(data.displays || []); + displaysCache.update(data.displays || []); } } catch (error) { console.error('Error refreshing displays for engine:', error); @@ -1841,8 +1810,7 @@ export async function showTestPPTemplateModal(templateId) { select.innerHTML = ''; if (_cachedStreams.length === 0) { try { - const resp = await fetchWithAuth('/picture-sources'); - if (resp.ok) { const d = await resp.json(); set_cachedStreams(d.streams || []); } + await streamsCache.fetch(); } catch (e) { console.warn('Could not load streams for PP test:', e); } } for (const s of _cachedStreams) { @@ -1894,24 +1862,13 @@ export function runPPTemplateTest() { // ===== PP Templates ===== async function loadAvailableFilters() { - try { - const response = await fetchWithAuth('/filters'); - if (!response.ok) throw new Error(`Failed to load filters: ${response.status}`); - const data = await response.json(); - set_availableFilters(data.filters || []); - } catch (error) { - console.error('Error loading available filters:', error); - set_availableFilters([]); - } + await filtersCache.fetch(); } async function loadPPTemplates() { try { - if (_availableFilters.length === 0) await loadAvailableFilters(); - const response = await fetchWithAuth('/postprocessing-templates'); - if (!response.ok) throw new Error(`Failed to load templates: ${response.status}`); - const data = await response.json(); - set_cachedPPTemplates(data.templates || []); + if (_availableFilters.length === 0) await filtersCache.fetch(); + await ppTemplatesCache.fetch(); renderPictureSourcesList(_cachedStreams); } catch (error) { console.error('Error loading PP templates:', error); diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 7a70609..ea402f9 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -8,7 +8,7 @@ import { _deviceBrightnessCache, kcWebSockets, ledPreviewWebSockets, - _cachedValueSources, set_cachedValueSources, + _cachedValueSources, valueSourcesCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -241,18 +241,14 @@ function _populateBrightnessVsDropdown(selectedId = '') { export async function showTargetEditor(targetId = null, cloneData = null) { try { // Load devices, CSS sources, and value sources for dropdowns - const [devicesResp, cssResp, vsResp] = await Promise.all([ + const [devicesResp, cssResp] = await Promise.all([ fetch(`${API_BASE}/devices`, { headers: getHeaders() }), fetchWithAuth('/color-strip-sources'), - fetchWithAuth('/value-sources'), + valueSourcesCache.fetch(), ]); const devices = devicesResp.ok ? (await devicesResp.json()).devices || [] : []; const cssSources = cssResp.ok ? (await cssResp.json()).sources || [] : []; - if (vsResp.ok) { - const vsData = await vsResp.json(); - set_cachedValueSources(vsData.sources || []); - } set_targetEditorDevices(devices); _editorCssSources = cssSources; @@ -478,13 +474,13 @@ export async function loadTargetsTab() { try { // Fetch devices, targets, CSS sources, picture sources, pattern templates, and value sources in parallel - const [devicesResp, targetsResp, cssResp, psResp, patResp, vsResp, asResp] = await Promise.all([ + const [devicesResp, targetsResp, cssResp, psResp, patResp, valueSrcArr, asResp] = await Promise.all([ fetchWithAuth('/devices'), fetchWithAuth('/picture-targets'), fetchWithAuth('/color-strip-sources').catch(() => null), fetchWithAuth('/picture-sources').catch(() => null), fetchWithAuth('/pattern-templates').catch(() => null), - fetchWithAuth('/value-sources').catch(() => null), + valueSourcesCache.fetch().catch(() => []), fetchWithAuth('/audio-sources').catch(() => null), ]); @@ -515,10 +511,7 @@ export async function loadTargetsTab() { } let valueSourceMap = {}; - if (vsResp && vsResp.ok) { - const vsData = await vsResp.json(); - (vsData.sources || []).forEach(s => { valueSourceMap[s.id] = s; }); - } + valueSrcArr.forEach(s => { valueSourceMap[s.id] = s; }); let audioSourceMap = {}; if (asResp && asResp.ok) { diff --git a/server/src/wled_controller/static/js/features/value-sources.js b/server/src/wled_controller/static/js/features/value-sources.js index c318fcd..721ceae 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -10,7 +10,7 @@ * This module manages the editor modal and API operations. */ -import { _cachedValueSources, set_cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey } from '../core/state.js'; +import { _cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey } from '../core/state.js'; import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js';