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';