Rework API input CSS: segments, remove led_count, HAOS light, test preview

API Input CSS rework:
- Remove led_count field from ApiInputColorStripSource (always auto-sizes)
- Add segment-based payload: solid, per_pixel, gradient modes
- Segments applied in order (last wins on overlap), auto-grow buffer
- Backward compatible: legacy {"colors": [...]} still works
- Pydantic validation: mode-specific field requirements

Test preview:
- Enable test preview button on api_input cards
- Hide LED/FPS controls for api_input (sender controls those)
- Show input source selector for all CSS tests (preselected)
- FPS sparkline chart using shared createFpsSparkline (same as target cards)
- Server only sends frames when push_generation changes (no idle frames)

HAOS integration:
- New light.py: ApiInputLight entity per api_input source (RGB + brightness)
- turn_on pushes solid segment, turn_off pushes fallback color
- Register wled_screen_controller.set_leds service for arbitrary segments
- New services.yaml with field definitions
- Coordinator: push_colors() and push_segments() methods
- Platform.LIGHT added to platforms list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 14:47:42 +03:00
parent 823cb90d2d
commit 8a6ffca446
25 changed files with 1085 additions and 326 deletions

View File

@@ -10,7 +10,7 @@
* This module manages the editor modal and API operations.
*/
import { _cachedAudioSources, _cachedAudioTemplates, apiKey } from '../core/state.js';
import { _cachedAudioSources, _cachedAudioTemplates, apiKey, audioSourcesCache } 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';
@@ -155,6 +155,7 @@ export async function saveAudioSource() {
}
showToast(t(id ? 'audio_source.updated' : 'audio_source.created'), 'success');
audioSourceModal.forceClose();
audioSourcesCache.invalidate();
await loadPictureSources();
} catch (e) {
errorEl.textContent = e.message;
@@ -205,6 +206,7 @@ export async function deleteAudioSource(sourceId) {
throw new Error(err.detail || `HTTP ${resp.status}`);
}
showToast(t('audio_source.deleted'), 'success');
audioSourcesCache.invalidate();
await loadPictureSources();
} catch (e) {
showToast(e.message, 'error');

View File

@@ -706,6 +706,7 @@ export async function saveAutomationEditor() {
automationModal.forceClose();
showToast(isEdit ? t('automations.updated') : t('automations.created'), 'success');
automationsCacheObj.invalidate();
loadAutomations();
} catch (e) {
if (e.isAuth) return;
@@ -720,6 +721,7 @@ export async function toggleAutomationEnabled(automationId, enable) {
method: 'POST',
});
if (!resp.ok) throw new Error(`Failed to ${action} automation`);
automationsCacheObj.invalidate();
loadAutomations();
} catch (e) {
if (e.isAuth) return;
@@ -767,6 +769,7 @@ export async function deleteAutomation(automationId, automationName) {
});
if (!resp.ok) throw new Error('Failed to delete automation');
showToast(t('automations.deleted'), 'success');
automationsCacheObj.invalidate();
loadAutomations();
} catch (e) {
if (e.isAuth) return;

View File

@@ -3,6 +3,7 @@
*/
import { fetchWithAuth, escapeHtml } from '../core/api.js';
import { createFpsSparkline } from '../core/chart-utils.js';
import { _cachedSyncClocks, _cachedValueSources, _cachedCSPTemplates, audioSourcesCache, streamsCache, colorStripSourcesCache, valueSourcesCache, csptCache } from '../core/state.js';
import { t } from '../core/i18n.js';
import { showToast, showConfirm, desktopFocus } from '../core/ui.js';
@@ -198,8 +199,8 @@ export function onCSSTypeChange() {
}
_syncAnimationSpeedState();
// LED count — only shown for picture, picture_advanced, api_input
const hasLedCount = ['picture', 'picture_advanced', 'api_input'];
// LED count — only shown for picture, picture_advanced
const hasLedCount = ['picture', 'picture_advanced'];
document.getElementById('css-editor-led-count-group').style.display =
hasLedCount.includes(type) ? '' : 'none';
@@ -1656,9 +1657,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
const notifHistoryBtn = isNotification
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); showNotificationHistory()" title="${t('color_strip.notification.history.title')}">${ICON_AUTOMATION}</button>`
: '';
const testPreviewBtn = !isApiInput
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testColorStrip('${source.id}')" title="${t('color_strip.test.title')}">${ICON_TEST}</button>`
: '';
const testPreviewBtn = `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testColorStrip('${source.id}')" title="${t('color_strip.test.title')}">${ICON_TEST}</button>`;
return wrapCard({
dataAttr: 'data-css-id',
@@ -2259,6 +2258,7 @@ export async function saveCSSEditor() {
showToast(cssId ? t('color_strip.updated') : t('color_strip.created'), 'success');
colorStripSourcesCache.invalidate();
cssEditorModal.forceClose();
if (window.loadPictureSources) window.loadPictureSources();
if (window.loadTargetsTab) await window.loadTargetsTab();
} catch (error) {
if (error.isAuth) return;
@@ -2335,6 +2335,7 @@ export async function deleteColorStrip(cssId) {
if (response.ok) {
showToast(t('color_strip.deleted'), 'success');
colorStripSourcesCache.invalidate();
if (window.loadPictureSources) window.loadPictureSources();
if (window.loadTargetsTab) await window.loadTargetsTab();
} else {
const err = await response.json();
@@ -2476,6 +2477,11 @@ let _cssTestGeneration = 0; // bumped on each connect to ignore stale WS messa
let _cssTestNotificationIds = []; // notification source IDs to fire (self or composite layers)
let _cssTestCSPTMode = false; // true when testing a CSPT template
let _cssTestCSPTId = null; // CSPT template ID when in CSPT mode
let _cssTestIsApiInput = false;
let _cssTestFpsTimestamps = []; // raw timestamps for current-second FPS calculation
let _cssTestFpsActualHistory = []; // rolling FPS samples for sparkline
let _cssTestFpsChart = null;
const _CSS_TEST_FPS_MAX_SAMPLES = 30;
let _csptTestInputEntitySelect = null;
function _getCssTestLedCount() {
@@ -2488,12 +2494,32 @@ function _getCssTestFps() {
return (stored >= 1 && stored <= 60) ? stored : 20;
}
function _populateCssTestSourceSelector(preselectId) {
const sources = colorStripSourcesCache.data || [];
const nonProcessed = sources.filter(s => s.source_type !== 'processed');
const sel = document.getElementById('css-test-cspt-input-select');
sel.innerHTML = nonProcessed.map(s =>
`<option value="${s.id}"${s.id === preselectId ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
).join('');
if (_csptTestInputEntitySelect) _csptTestInputEntitySelect.destroy();
_csptTestInputEntitySelect = new EntitySelect({
target: sel,
getItems: () => (colorStripSourcesCache.data || [])
.filter(s => s.source_type !== 'processed')
.map(s => ({ value: s.id, label: s.name, icon: getColorStripIcon(s.source_type) })),
placeholder: t('palette.search'),
});
}
export function testColorStrip(sourceId) {
_cssTestCSPTMode = false;
_cssTestCSPTId = null;
// Hide CSPT input selector
const csptGroup = document.getElementById('css-test-cspt-input-group');
if (csptGroup) csptGroup.style.display = 'none';
// Detect api_input type
const sources = colorStripSourcesCache.data || [];
const src = sources.find(s => s.id === sourceId);
_cssTestIsApiInput = src?.source_type === 'api_input';
// Populate input source selector with current source preselected
_populateCssTestSourceSelector(sourceId);
_openTestModal(sourceId);
}
@@ -2503,25 +2529,9 @@ export async function testCSPT(templateId) {
// Populate input source selector
await colorStripSourcesCache.fetch();
const sources = colorStripSourcesCache.data || [];
const nonProcessed = sources.filter(s => s.source_type !== 'processed');
const sel = document.getElementById('css-test-cspt-input-select');
sel.innerHTML = nonProcessed.map(s =>
`<option value="${s.id}">${escapeHtml(s.name)}</option>`
).join('');
// EntitySelect for input source picker
if (_csptTestInputEntitySelect) _csptTestInputEntitySelect.destroy();
_csptTestInputEntitySelect = new EntitySelect({
target: sel,
getItems: () => (colorStripSourcesCache.data || [])
.filter(s => s.source_type !== 'processed')
.map(s => ({ value: s.id, label: s.name, icon: getColorStripIcon(s.source_type) })),
placeholder: t('palette.search'),
});
// Show CSPT input selector
const csptGroup = document.getElementById('css-test-cspt-input-group');
if (csptGroup) csptGroup.style.display = '';
_populateCssTestSourceSelector(null);
const sel = document.getElementById('css-test-cspt-input-select');
const inputId = sel.value;
if (!inputId) {
showToast(t('color_strip.processed.error.no_input'), 'error');
@@ -2550,23 +2560,42 @@ function _openTestModal(sourceId) {
document.getElementById('css-test-rect-view').style.display = 'none';
document.getElementById('css-test-layers-view').style.display = 'none';
document.getElementById('css-test-led-group').style.display = '';
// Input source selector: shown for both CSS test and CSPT test, hidden for api_input
const csptGroup = document.getElementById('css-test-cspt-input-group');
if (csptGroup) csptGroup.style.display = _cssTestIsApiInput ? 'none' : '';
const layersContainer = document.getElementById('css-test-layers');
if (layersContainer) layersContainer.innerHTML = '';
document.getElementById('css-test-status').style.display = '';
document.getElementById('css-test-status').textContent = t('color_strip.test.connecting');
// Restore LED count + FPS + Enter key handlers
const ledCount = _getCssTestLedCount();
const ledInput = document.getElementById('css-test-led-input');
ledInput.value = ledCount;
ledInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
// Reset FPS tracking
_cssTestFpsHistory = [];
const fpsVal = _getCssTestFps();
const fpsInput = document.getElementById('css-test-fps-input');
fpsInput.value = fpsVal;
fpsInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
// For api_input: hide LED/FPS controls, show FPS chart
const ledControlGroup = document.getElementById('css-test-led-fps-group');
const fpsChartGroup = document.getElementById('css-test-fps-chart-group');
if (_cssTestIsApiInput) {
if (ledControlGroup) ledControlGroup.style.display = 'none';
if (fpsChartGroup) fpsChartGroup.style.display = '';
_cssTestStartFpsSampling();
// Use large LED count (buffer auto-sizes) and high poll FPS
_cssTestConnect(sourceId, 1000, 60);
} else {
if (ledControlGroup) ledControlGroup.style.display = '';
if (fpsChartGroup) fpsChartGroup.style.display = 'none';
// Restore LED count + FPS + Enter key handlers
const ledCount = _getCssTestLedCount();
const ledInput = document.getElementById('css-test-led-input');
ledInput.value = ledCount;
ledInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
_cssTestConnect(sourceId, ledCount, fpsVal);
const fpsVal = _getCssTestFps();
const fpsInput = document.getElementById('css-test-fps-input');
fpsInput.value = fpsVal;
fpsInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
_cssTestConnect(sourceId, ledCount, fpsVal);
}
}
function _cssTestConnect(sourceId, ledCount, fps) {
@@ -2714,6 +2743,11 @@ function _cssTestConnect(sourceId, ledCount, fps) {
// Standard format: raw RGB
_cssTestLatestRgb = raw;
}
// Track FPS for api_input sources
if (_cssTestIsApiInput) {
_cssTestFpsTimestamps.push(performance.now());
}
}
};
@@ -2805,12 +2839,14 @@ export function applyCssTestSettings() {
_cssTestMeta = null;
_cssTestLayerData = null;
// In CSPT mode, read selected input source
if (_cssTestCSPTMode) {
const inputSel = document.getElementById('css-test-cspt-input-select');
if (inputSel && inputSel.value) {
_cssTestSourceId = inputSel.value;
}
// Read selected input source from selector (both CSS and CSPT modes)
const inputSel = document.getElementById('css-test-cspt-input-select');
if (inputSel && inputSel.value) {
_cssTestSourceId = inputSel.value;
// Re-detect api_input when source changes
const sources = colorStripSourcesCache.data || [];
const src = sources.find(s => s.id === _cssTestSourceId);
_cssTestIsApiInput = src?.source_type === 'api_input';
}
// Reconnect (generation counter ignores stale frames from old WS)
@@ -3162,6 +3198,60 @@ export function fireCssTestNotificationLayer(sourceId) {
testNotification(sourceId);
}
let _cssTestFpsSampleInterval = null;
function _cssTestStartFpsSampling() {
_cssTestStopFpsSampling();
_cssTestFpsTimestamps = [];
_cssTestFpsActualHistory = [];
if (_cssTestFpsChart) { _cssTestFpsChart.destroy(); _cssTestFpsChart = null; }
// Sample FPS every 1 second
_cssTestFpsSampleInterval = setInterval(() => {
const now = performance.now();
// Count frames in the last 1 second
const cutoff = now - 1000;
_cssTestFpsTimestamps = _cssTestFpsTimestamps.filter(t => t >= cutoff);
const fps = _cssTestFpsTimestamps.length;
_cssTestFpsActualHistory.push(fps);
if (_cssTestFpsActualHistory.length > _CSS_TEST_FPS_MAX_SAMPLES)
_cssTestFpsActualHistory.shift();
// Update numeric display (match target card format)
const valueEl = document.getElementById('css-test-fps-value');
if (valueEl) valueEl.textContent = fps;
const avgEl = document.getElementById('css-test-fps-avg');
if (avgEl && _cssTestFpsActualHistory.length > 1) {
const avg = _cssTestFpsActualHistory.reduce((a, b) => a + b, 0) / _cssTestFpsActualHistory.length;
avgEl.textContent = `avg ${avg.toFixed(1)}`;
}
// Create or update chart
if (!_cssTestFpsChart) {
_cssTestFpsChart = createFpsSparkline(
'css-test-fps-chart',
_cssTestFpsActualHistory,
[], // no "current" dataset, just actual
60, // y-axis max
);
}
if (_cssTestFpsChart) {
const ds = _cssTestFpsChart.data.datasets[0].data;
ds.length = 0;
ds.push(..._cssTestFpsActualHistory);
while (_cssTestFpsChart.data.labels.length < ds.length) _cssTestFpsChart.data.labels.push('');
_cssTestFpsChart.data.labels.length = ds.length;
_cssTestFpsChart.update('none');
}
}, 1000);
}
function _cssTestStopFpsSampling() {
if (_cssTestFpsSampleInterval) { clearInterval(_cssTestFpsSampleInterval); _cssTestFpsSampleInterval = null; }
if (_cssTestFpsChart) { _cssTestFpsChart.destroy(); _cssTestFpsChart = null; }
}
export function closeTestCssSourceModal() {
if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; }
if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; }
@@ -3171,6 +3261,10 @@ export function closeTestCssSourceModal() {
_cssTestIsComposite = false;
_cssTestLayerData = null;
_cssTestNotificationIds = [];
_cssTestIsApiInput = false;
_cssTestStopFpsSampling();
_cssTestFpsTimestamps = [];
_cssTestFpsActualHistory = [];
// Revoke blob URL for frame preview
const screen = document.getElementById('css-test-rect-screen');
if (screen && screen._blobUrl) { URL.revokeObjectURL(screen._blobUrl); screen._blobUrl = null; screen.style.backgroundImage = ''; }

View File

@@ -243,6 +243,7 @@ export async function saveScenePreset() {
scenePresetModal.forceClose();
showToast(_editingId ? t('scenes.updated') : t('scenes.captured'), 'success');
scenePresetsCache.invalidate();
_reloadScenesTab();
} catch (error) {
if (error.isAuth) return;
@@ -348,6 +349,7 @@ export async function recaptureScenePreset(presetId) {
});
if (resp.ok) {
showToast(t('scenes.recaptured'), 'success');
scenePresetsCache.invalidate();
_reloadScenesTab();
} else {
showToast(t('scenes.error.recapture_failed'), 'error');
@@ -420,6 +422,7 @@ export async function deleteScenePreset(presetId) {
});
if (resp.ok) {
showToast(t('scenes.deleted'), 'success');
scenePresetsCache.invalidate();
_reloadScenesTab();
} else {
showToast(t('scenes.error.delete_failed'), 'error');

View File

@@ -1,5 +1,5 @@
/**
* Settings — backup / restore configuration.
* Settings — tabbed modal (General / Backup / MQTT) + full-screen Log overlay.
*/
import { apiKey } from '../core/state.js';
@@ -10,6 +10,17 @@ import { t } from '../core/i18n.js';
import { ICON_UNDO, ICON_DOWNLOAD } from '../core/icons.js';
import { IconSelect } from '../core/icon-select.js';
// ─── Settings-modal tab switching ───────────────────────────
export function switchSettingsTab(tabId) {
document.querySelectorAll('.settings-tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.settingsTab === tabId);
});
document.querySelectorAll('.settings-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === `settings-panel-${tabId}`);
});
}
// ─── Log Viewer ────────────────────────────────────────────
/** @type {WebSocket|null} */
@@ -114,9 +125,6 @@ export function clearLogViewer() {
/** Re-render the log output according to the current filter selection. */
export function applyLogFilter() {
// We don't buffer all raw lines in JS — just clear and note the filter
// will apply to future lines. Existing lines that were already rendered
// are re-evaluated by toggling their visibility.
const output = document.getElementById('log-viewer-output');
if (!output) return;
const filter = _filterLevel();
@@ -128,50 +136,84 @@ export function applyLogFilter() {
}
}
// ─── Log Overlay (full-screen) ──────────────────────────────
let _logFilterIconSelect = null;
/** Build filter items lazily so t() has locale data loaded. */
function _getLogFilterItems() {
return [
{ value: 'all', icon: '<span style="color:#9e9e9e;font-weight:700">*</span>', label: t('settings.logs.filter.all'), desc: t('settings.logs.filter.all_desc') },
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: t('settings.logs.filter.info'), desc: t('settings.logs.filter.info_desc') },
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: t('settings.logs.filter.warning'), desc: t('settings.logs.filter.warning_desc') },
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: t('settings.logs.filter.error'), desc: t('settings.logs.filter.error_desc') },
];
}
export function openLogOverlay() {
const overlay = document.getElementById('log-overlay');
if (overlay) {
overlay.style.display = 'flex';
// Initialize log filter icon select (once)
if (!_logFilterIconSelect) {
const filterSel = document.getElementById('log-viewer-filter');
if (filterSel) {
_logFilterIconSelect = new IconSelect({
target: filterSel,
items: _getLogFilterItems(),
columns: 2,
onChange: () => applyLogFilter(),
});
}
}
// Auto-connect when opening
if (!_logWs || _logWs.readyState !== WebSocket.OPEN) {
connectLogViewer();
}
}
}
export function closeLogOverlay() {
const overlay = document.getElementById('log-overlay');
if (overlay) overlay.style.display = 'none';
disconnectLogViewer();
}
// ─── Settings Modal ─────────────────────────────────────────
// Simple modal (no form / no dirty check needed)
const settingsModal = new Modal('settings-modal');
let _logFilterIconSelect = null;
let _logLevelIconSelect = null;
const _LOG_LEVEL_ITEMS = [
{ value: 'DEBUG', icon: '<span style="color:#9e9e9e;font-weight:700">D</span>', label: 'DEBUG', desc: t('settings.log_level.desc.debug') },
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: 'INFO', desc: t('settings.log_level.desc.info') },
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: 'WARNING', desc: t('settings.log_level.desc.warning') },
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: 'ERROR', desc: t('settings.log_level.desc.error') },
{ value: 'CRITICAL', icon: '<span style="color:#ff1744;font-weight:700">!</span>', label: 'CRITICAL', desc: t('settings.log_level.desc.critical') },
];
const _LOG_FILTER_ITEMS = [
{ value: 'all', icon: '<span style="color:#9e9e9e;font-weight:700">*</span>', label: t('settings.logs.filter.all'), desc: t('settings.logs.filter.all_desc') },
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: t('settings.logs.filter.info'), desc: t('settings.logs.filter.info_desc') },
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: t('settings.logs.filter.warning'), desc: t('settings.logs.filter.warning_desc') },
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: t('settings.logs.filter.error'), desc: t('settings.logs.filter.error_desc') },
];
/** Build log-level items lazily so t() has locale data loaded. */
function _getLogLevelItems() {
return [
{ value: 'DEBUG', icon: '<span style="color:#9e9e9e;font-weight:700">D</span>', label: 'DEBUG', desc: t('settings.log_level.desc.debug') },
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: 'INFO', desc: t('settings.log_level.desc.info') },
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: 'WARNING', desc: t('settings.log_level.desc.warning') },
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: 'ERROR', desc: t('settings.log_level.desc.error') },
{ value: 'CRITICAL', icon: '<span style="color:#ff1744;font-weight:700">!</span>', label: 'CRITICAL', desc: t('settings.log_level.desc.critical') },
];
}
export function openSettingsModal() {
document.getElementById('settings-error').style.display = 'none';
// Reset to first tab
switchSettingsTab('general');
settingsModal.open();
// Initialize log filter icon select
if (!_logFilterIconSelect) {
const filterSel = document.getElementById('log-viewer-filter');
if (filterSel) {
_logFilterIconSelect = new IconSelect({
target: filterSel,
items: _LOG_FILTER_ITEMS,
columns: 2,
onChange: () => applyLogFilter(),
});
}
}
// Initialize log level icon select
if (!_logLevelIconSelect) {
const levelSel = document.getElementById('settings-log-level');
if (levelSel) {
_logLevelIconSelect = new IconSelect({
target: levelSel,
items: _LOG_LEVEL_ITEMS,
items: _getLogLevelItems(),
columns: 3,
onChange: () => setLogLevel(),
});
@@ -186,7 +228,6 @@ export function openSettingsModal() {
}
export function closeSettingsModal() {
disconnectLogViewer();
settingsModal.forceClose();
}

View File

@@ -710,6 +710,7 @@ export async function saveTemplate() {
showToast(templateId ? t('templates.updated') : t('templates.created'), 'success');
templateModal.forceClose();
captureTemplatesCache.invalidate();
await loadCaptureTemplates();
} catch (error) {
console.error('Error saving template:', error);
@@ -729,6 +730,7 @@ export async function deleteTemplate(templateId) {
throw new Error(error.detail || error.message || 'Failed to delete template');
}
showToast(t('templates.deleted'), 'success');
captureTemplatesCache.invalidate();
await loadCaptureTemplates();
} catch (error) {
console.error('Error deleting template:', error);
@@ -970,6 +972,7 @@ export async function saveAudioTemplate() {
showToast(templateId ? t('audio_template.updated') : t('audio_template.created'), 'success');
audioTemplateModal.forceClose();
audioTemplatesCache.invalidate();
await loadAudioTemplates();
} catch (error) {
console.error('Error saving audio template:', error);
@@ -989,6 +992,7 @@ export async function deleteAudioTemplate(templateId) {
throw new Error(error.detail || error.message || 'Failed to delete audio template');
}
showToast(t('audio_template.deleted'), 'success');
audioTemplatesCache.invalidate();
await loadAudioTemplates();
} catch (error) {
console.error('Error deleting audio template:', error);
@@ -2089,6 +2093,7 @@ export async function saveStream() {
showToast(streamId ? t('streams.updated') : t('streams.created'), 'success');
streamModal.forceClose();
streamsCache.invalidate();
await loadPictureSources();
} catch (error) {
console.error('Error saving stream:', error);
@@ -2108,6 +2113,7 @@ export async function deleteStream(streamId) {
throw new Error(error.detail || error.message || 'Failed to delete stream');
}
showToast(t('streams.deleted'), 'success');
streamsCache.invalidate();
await loadPictureSources();
} catch (error) {
console.error('Error deleting stream:', error);
@@ -2675,6 +2681,7 @@ export async function savePPTemplate() {
showToast(templateId ? t('postprocessing.updated') : t('postprocessing.created'), 'success');
ppTemplateModal.forceClose();
ppTemplatesCache.invalidate();
await loadPPTemplates();
} catch (error) {
console.error('Error saving PP template:', error);
@@ -2735,6 +2742,7 @@ export async function deletePPTemplate(templateId) {
throw new Error(error.detail || error.message || 'Failed to delete template');
}
showToast(t('postprocessing.deleted'), 'success');
ppTemplatesCache.invalidate();
await loadPPTemplates();
} catch (error) {
console.error('Error deleting PP template:', error);
@@ -2888,6 +2896,7 @@ export async function saveCSPT() {
showToast(templateId ? t('css_processing.updated') : t('css_processing.created'), 'success');
csptModal.forceClose();
csptCache.invalidate();
await loadCSPTemplates();
} catch (error) {
console.error('Error saving CSPT:', error);
@@ -2920,6 +2929,7 @@ export async function deleteCSPT(templateId) {
throw new Error(error.detail || error.message || 'Failed to delete template');
}
showToast(t('css_processing.deleted'), 'success');
csptCache.invalidate();
await loadCSPTemplates();
} catch (error) {
console.error('Error deleting CSPT:', error);

View File

@@ -98,6 +98,7 @@ export async function saveSyncClock() {
}
showToast(t(id ? 'sync_clock.updated' : 'sync_clock.created'), 'success');
syncClockModal.forceClose();
syncClocksCache.invalidate();
await loadPictureSources();
} catch (e) {
if (e.isAuth) return;
@@ -143,6 +144,7 @@ export async function deleteSyncClock(clockId) {
throw new Error(err.detail || `HTTP ${resp.status}`);
}
showToast(t('sync_clock.deleted'), 'success');
syncClocksCache.invalidate();
await loadPictureSources();
} catch (e) {
if (e.isAuth) return;

View File

@@ -10,7 +10,7 @@
* This module manages the editor modal and API operations.
*/
import { _cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey } from '../core/state.js';
import { _cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey, valueSourcesCache } 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';
@@ -486,6 +486,7 @@ export async function saveValueSource() {
}
showToast(t(id ? 'value_source.updated' : 'value_source.created'), 'success');
valueSourceModal.forceClose();
valueSourcesCache.invalidate();
await loadPictureSources();
} catch (e) {
errorEl.textContent = e.message;
@@ -536,6 +537,7 @@ export async function deleteValueSource(sourceId) {
throw new Error(err.detail || `HTTP ${resp.status}`);
}
showToast(t('value_source.deleted'), 'success');
valueSourcesCache.invalidate();
await loadPictureSources();
} catch (e) {
showToast(e.message, 'error');