diff --git a/server/src/ledgrab/static/js/core/modal.ts b/server/src/ledgrab/static/js/core/modal.ts index acd8c64..1ad6dc8 100644 --- a/server/src/ledgrab/static/js/core/modal.ts +++ b/server/src/ledgrab/static/js/core/modal.ts @@ -156,6 +156,24 @@ export class Modal { return Object.keys(this._initialValues).some(k => this._initialValues[k] !== cur[k]); } + /** + * No-op save guard for edit modals. When `entityId` is truthy (we are + * editing an existing entity), snapshot tracking is configured, and no + * tracked field has changed, force-close the modal silently and return + * `true` so the caller can early-return — skipping the network request + * and the misleading "updated" toast. + * + * Returns `false` when the save flow must continue (create flow, no + * snapshot taken, or at least one tracked field changed). + */ + closeIfPristine(entityId: unknown): boolean { + if (!entityId) return false; + if (Object.keys(this._initialValues).length === 0) return false; + if (this.isDirty()) return false; + this.forceClose(); + return true; + } + showError(msg: string) { if (this.errorEl) { this.errorEl.textContent = msg; diff --git a/server/src/ledgrab/static/js/features/advanced-calibration.ts b/server/src/ledgrab/static/js/features/advanced-calibration.ts index a2c1832..b2fc05c 100644 --- a/server/src/ledgrab/static/js/features/advanced-calibration.ts +++ b/server/src/ledgrab/static/js/features/advanced-calibration.ts @@ -199,6 +199,8 @@ export async function saveAdvancedCalibration(): Promise { const cssId = _state.cssId; if (!cssId) return; + if (_modal.closeIfPristine(cssId)) return; + if (_state.lines.length === 0) { showToast(t('calibration.advanced.no_lines_warning') || 'Add at least one line', 'error'); return; diff --git a/server/src/ledgrab/static/js/features/assets.ts b/server/src/ledgrab/static/js/features/assets.ts index 16d5320..4a4f25b 100644 --- a/server/src/ledgrab/static/js/features/assets.ts +++ b/server/src/ledgrab/static/js/features/assets.ts @@ -380,6 +380,8 @@ export async function showAssetEditor(editId: string): Promise { export async function saveAssetMetadata(): Promise { const id = (document.getElementById('asset-editor-id') as HTMLInputElement).value; + if (assetEditorModal.closeIfPristine(id)) return; + const name = (document.getElementById('asset-editor-name') as HTMLInputElement).value.trim(); const description = (document.getElementById('asset-editor-description') as HTMLInputElement).value.trim(); const errorEl = document.getElementById('asset-editor-error')!; diff --git a/server/src/ledgrab/static/js/features/audio-processing-templates.ts b/server/src/ledgrab/static/js/features/audio-processing-templates.ts index cff8525..4f19507 100644 --- a/server/src/ledgrab/static/js/features/audio-processing-templates.ts +++ b/server/src/ledgrab/static/js/features/audio-processing-templates.ts @@ -194,6 +194,8 @@ export async function editAudioProcessingTemplate(templateId: string) { export async function saveAudioProcessingTemplate() { const templateId = (document.getElementById('apt-id') as HTMLInputElement).value; + if (aptModal.closeIfPristine(templateId)) return; + const name = (document.getElementById('apt-name') as HTMLInputElement).value.trim(); const description = (document.getElementById('apt-description') as HTMLInputElement).value.trim(); diff --git a/server/src/ledgrab/static/js/features/audio-sources.ts b/server/src/ledgrab/static/js/features/audio-sources.ts index 4c06ec2..8c879c4 100644 --- a/server/src/ledgrab/static/js/features/audio-sources.ts +++ b/server/src/ledgrab/static/js/features/audio-sources.ts @@ -153,6 +153,8 @@ export function onAudioSourceTypeChange() { export async function saveAudioSource() { const id = (document.getElementById('audio-source-id') as HTMLInputElement).value; + if (audioSourceModal.closeIfPristine(id)) return; + const name = (document.getElementById('audio-source-name') as HTMLInputElement).value.trim(); const sourceType = (document.getElementById('audio-source-type') as HTMLSelectElement).value; const description = (document.getElementById('audio-source-description') as HTMLInputElement).value.trim() || null; diff --git a/server/src/ledgrab/static/js/features/calibration.ts b/server/src/ledgrab/static/js/features/calibration.ts index 637bb20..ec28860 100644 --- a/server/src/ledgrab/static/js/features/calibration.ts +++ b/server/src/ledgrab/static/js/features/calibration.ts @@ -914,6 +914,8 @@ export async function saveCalibration() { const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement).value; const error = document.getElementById('calibration-error') as HTMLElement; + if (calibModal.closeIfPristine(cssMode ? cssId : deviceId)) return; + if (cssMode) { await _clearCSSTestMode(); } else { diff --git a/server/src/ledgrab/static/js/features/devices.ts b/server/src/ledgrab/static/js/features/devices.ts index 59b2138..67fb188 100644 --- a/server/src/ledgrab/static/js/features/devices.ts +++ b/server/src/ledgrab/static/js/features/devices.ts @@ -843,6 +843,8 @@ export function closeDeviceSettingsModal() { settingsModal.close(); } export async function saveDeviceSettings() { const deviceId = (document.getElementById('settings-device-id') as HTMLInputElement).value; + if (settingsModal.closeIfPristine(deviceId)) return; + const name = (document.getElementById('settings-device-name') as HTMLInputElement).value.trim(); const url = settingsModal._getUrl(); diff --git a/server/src/ledgrab/static/js/features/game-integration.ts b/server/src/ledgrab/static/js/features/game-integration.ts index 271a989..e8f4970 100644 --- a/server/src/ledgrab/static/js/features/game-integration.ts +++ b/server/src/ledgrab/static/js/features/game-integration.ts @@ -707,6 +707,8 @@ export async function showGameIntegrationEditor(editId: string | null = null) { export async function saveGameIntegration() { const id = (document.getElementById('gi-id') as HTMLInputElement).value; + if (giModal.closeIfPristine(id)) return; + const name = (document.getElementById('gi-name') as HTMLInputElement).value.trim(); if (!name) { giModal.showError(t('game_integration.error.name_required')); return; } diff --git a/server/src/ledgrab/static/js/features/ha-light-targets.ts b/server/src/ledgrab/static/js/features/ha-light-targets.ts index c876005..0fc6b9f 100644 --- a/server/src/ledgrab/static/js/features/ha-light-targets.ts +++ b/server/src/ledgrab/static/js/features/ha-light-targets.ts @@ -487,6 +487,8 @@ export async function closeHALightEditor(): Promise { export async function saveHALightEditor(): Promise { const targetId = (document.getElementById('ha-light-editor-id') as HTMLInputElement).value; + if (haLightEditorModal.closeIfPristine(targetId)) return; + const name = (document.getElementById('ha-light-editor-name') as HTMLInputElement).value.trim(); const haSourceId = (document.getElementById('ha-light-editor-ha-source') as HTMLSelectElement).value; const colorSourceRaw = (document.getElementById('ha-light-editor-css-source') as HTMLSelectElement).value; diff --git a/server/src/ledgrab/static/js/features/home-assistant-sources.ts b/server/src/ledgrab/static/js/features/home-assistant-sources.ts index 364c634..ae96bff 100644 --- a/server/src/ledgrab/static/js/features/home-assistant-sources.ts +++ b/server/src/ledgrab/static/js/features/home-assistant-sources.ts @@ -142,6 +142,8 @@ export async function closeHASourceModal(): Promise { export async function saveHASource(): Promise { const id = (document.getElementById('ha-source-id') as HTMLInputElement).value; + if (haSourceModal.closeIfPristine(id)) return; + const name = (document.getElementById('ha-source-name') as HTMLInputElement).value.trim(); const host = (document.getElementById('ha-source-host') as HTMLInputElement).value.trim(); const token = (document.getElementById('ha-source-token') as HTMLInputElement).value.trim(); diff --git a/server/src/ledgrab/static/js/features/mqtt-sources.ts b/server/src/ledgrab/static/js/features/mqtt-sources.ts index 837cc60..cb503f4 100644 --- a/server/src/ledgrab/static/js/features/mqtt-sources.ts +++ b/server/src/ledgrab/static/js/features/mqtt-sources.ts @@ -115,6 +115,8 @@ export async function closeMQTTSourceModal(): Promise { export async function saveMQTTSource(): Promise { const id = (document.getElementById('mqtt-source-id') as HTMLInputElement).value; + if (mqttSourceModal.closeIfPristine(id)) return; + const name = (document.getElementById('mqtt-source-name') as HTMLInputElement).value.trim(); const broker_host = (document.getElementById('mqtt-source-host') as HTMLInputElement).value.trim(); const broker_port = parseInt((document.getElementById('mqtt-source-port') as HTMLInputElement).value, 10) || 1883; diff --git a/server/src/ledgrab/static/js/features/pattern-templates.ts b/server/src/ledgrab/static/js/features/pattern-templates.ts index 6180e2d..9b6f0e9 100644 --- a/server/src/ledgrab/static/js/features/pattern-templates.ts +++ b/server/src/ledgrab/static/js/features/pattern-templates.ts @@ -240,6 +240,8 @@ export async function savePatternTemplate(): Promise { } const templateId = (document.getElementById('pattern-template-id') as HTMLInputElement).value; + if (patternModal.closeIfPristine(templateId)) return; + const name = (document.getElementById('pattern-template-name') as HTMLInputElement).value.trim(); const description = (document.getElementById('pattern-template-description') as HTMLInputElement).value.trim(); diff --git a/server/src/ledgrab/static/js/features/scene-presets.ts b/server/src/ledgrab/static/js/features/scene-presets.ts index f7a2a34..0418763 100644 --- a/server/src/ledgrab/static/js/features/scene-presets.ts +++ b/server/src/ledgrab/static/js/features/scene-presets.ts @@ -369,6 +369,8 @@ export async function editScenePreset(presetId: string): Promise { // ===== Save (create or update) ===== export async function saveScenePreset(): Promise { + if (scenePresetModal.closeIfPristine(_editingId)) return; + const name = (document.getElementById('scene-preset-editor-name') as HTMLInputElement).value.trim(); const description = (document.getElementById('scene-preset-editor-description') as HTMLInputElement).value.trim(); const errorEl = document.getElementById('scene-preset-editor-error')!; diff --git a/server/src/ledgrab/static/js/features/sync-clocks.ts b/server/src/ledgrab/static/js/features/sync-clocks.ts index 0540e10..50e2212 100644 --- a/server/src/ledgrab/static/js/features/sync-clocks.ts +++ b/server/src/ledgrab/static/js/features/sync-clocks.ts @@ -110,6 +110,8 @@ export async function closeSyncClockModal(): Promise { export async function saveSyncClock(): Promise { const id = (document.getElementById('sync-clock-id') as HTMLInputElement).value; + if (syncClockModal.closeIfPristine(id)) return; + const name = (document.getElementById('sync-clock-name') as HTMLInputElement).value.trim(); const speed = parseFloat((document.getElementById('sync-clock-speed') as HTMLInputElement).value); const description = (document.getElementById('sync-clock-description') as HTMLInputElement).value.trim() || null; diff --git a/server/src/ledgrab/static/js/features/targets.ts b/server/src/ledgrab/static/js/features/targets.ts index c53e6d4..d1fe26a 100644 --- a/server/src/ledgrab/static/js/features/targets.ts +++ b/server/src/ledgrab/static/js/features/targets.ts @@ -496,6 +496,8 @@ export function forceCloseTargetEditorModal() { export async function saveTargetEditor() { const targetId = (document.getElementById('target-editor-id') as HTMLInputElement).value; + if (targetEditorModal.closeIfPristine(targetId)) return; + const name = (document.getElementById('target-editor-name') as HTMLInputElement).value.trim(); const deviceId = (document.getElementById('target-editor-device') as HTMLSelectElement).value; const standbyInterval = parseFloat((document.getElementById('target-editor-keepalive-interval') as HTMLInputElement).value); diff --git a/server/src/ledgrab/static/js/features/weather-sources.ts b/server/src/ledgrab/static/js/features/weather-sources.ts index 83af16f..4c1518b 100644 --- a/server/src/ledgrab/static/js/features/weather-sources.ts +++ b/server/src/ledgrab/static/js/features/weather-sources.ts @@ -146,6 +146,8 @@ export async function closeWeatherSourceModal(): Promise { export async function saveWeatherSource(): Promise { const id = (document.getElementById('weather-source-id') as HTMLInputElement).value; + if (weatherSourceModal.closeIfPristine(id)) return; + const name = (document.getElementById('weather-source-name') as HTMLInputElement).value.trim(); const provider = (document.getElementById('weather-source-provider') as HTMLSelectElement).value; const latitude = parseFloat((document.getElementById('weather-source-latitude') as HTMLInputElement).value) || 50.0;