From d94c012e0fb4871cb51ce4cece84e5ca80357871 Mon Sep 17 00:00:00 2001 From: Colby Serpa Date: Sun, 17 Aug 2025 02:30:18 -0700 Subject: [PATCH] fix: Use surgical updates for generic settings The previous implementation would send the entire settings section to the backend when any value was changed, which would trigger the configuration overwrite bug. This commit modifies the frontend to send individual setting updates instead, preventing the bug from being triggered. --- src/hooks/useGenericSettings.ts | 153 ++++++-------------------------- 1 file changed, 28 insertions(+), 125 deletions(-) diff --git a/src/hooks/useGenericSettings.ts b/src/hooks/useGenericSettings.ts index 46b86f7..f35c22e 100644 --- a/src/hooks/useGenericSettings.ts +++ b/src/hooks/useGenericSettings.ts @@ -346,6 +346,7 @@ interface UseGenericSettingsResult { fetchSettings: () => Promise; updateSettings: (updatedSettings: Partial) => void; saveSettings: () => Promise; + updateSetting: (key: string, value: any) => Promise; } /** @@ -437,130 +438,12 @@ const useGenericSettings = ( }); }, [groupName]); - const saveSettings = useCallback(async () => { - if (!settings) { - console.warn('No settings to save'); - return; - } - + const updateSetting = useCallback(async (key: string, value: any) => { try { setLoading(true); setError(null); - // By default, use settings as-is - let dataToSave = settings; - - // Define settings groups that need special handling - const prefixedSettingsMap: Record = { - 'image_moderation': { - prefix: 'image_moderation_', - formKeys: [ - 'image_moderation_api', - 'image_moderation_check_interval', - 'image_moderation_concurrency', - 'image_moderation_enabled', - 'image_moderation_mode', - 'image_moderation_temp_dir', - 'image_moderation_threshold', - 'image_moderation_timeout' - ] - }, - 'content_filter': { - prefix: 'content_filter_', - formKeys: [ - 'content_filter_cache_size', - 'content_filter_cache_ttl', - 'content_filter_enabled', - 'full_text_kinds' // Special case without prefix - ] - }, - 'ollama': { - prefix: 'ollama_', - formKeys: [ - 'ollama_model', - 'ollama_timeout', - 'ollama_url' - ] - }, - 'xnostr': { - prefix: 'xnostr_', - formKeys: [ - 'xnostr_browser_path', - 'xnostr_browser_pool_size', - 'xnostr_check_interval', - 'xnostr_concurrency', - 'xnostr_enabled', - 'xnostr_temp_dir', - 'xnostr_update_interval', - 'xnostr_nitter', - 'xnostr_verification_intervals' - ] - }, - 'wallet': { - prefix: 'wallet_', - formKeys: [ - 'wallet_api_key', - 'wallet_name' - ] - } - }; - - // Check if this group needs special handling - if (groupName in prefixedSettingsMap) { - console.log(`Settings from state for ${groupName}:`, settings); - const { prefix, formKeys } = prefixedSettingsMap[groupName]; - - // First fetch complete settings structure to preserve all values - console.log(`Fetching complete settings before saving ${groupName}...`); - const fetchResponse = await fetch(`${config.baseURL}/api/settings`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - - if (!fetchResponse.ok) { - throw new Error(`Failed to fetch current settings: ${fetchResponse.status}`); - } - - const currentData = await fetchResponse.json(); - const currentSettings = extractSettingsForGroup(currentData.settings, groupName) || {}; - console.log(`Current ${groupName} settings from API:`, currentSettings); - - // Create a properly prefixed object for the API - const prefixedSettings: Record = {}; - - // Copy all existing settings from the backend with correct prefixes - Object.entries(currentSettings).forEach(([key, value]) => { - // Special case for content_filter's full_text_kinds which doesn't have prefix - if (groupName === 'content_filter' && key === 'full_text_kinds') { - prefixedSettings[key] = value; - } else { - // Skip prefixing if key already has the prefix to avoid double-prefixing - const prefixedKey = key.startsWith(prefix) ? key : `${prefix}${key}`; - prefixedSettings[prefixedKey] = value; - } - }); - - // Update with changed values from the form - const settingsObj = settings as Record; - - // Update each field that has changed - formKeys.forEach(formKey => { - if (formKey in settingsObj && settingsObj[formKey] !== undefined) { - console.log(`Updating field: ${formKey} from ${prefixedSettings[formKey]} to ${settingsObj[formKey]}`); - prefixedSettings[formKey] = settingsObj[formKey]; - } - }); - - console.log(`Final ${groupName} settings with prefixed keys for API:`, prefixedSettings); - dataToSave = prefixedSettings as unknown as SettingsGroupType; - } - - console.log(`Saving ${groupName} settings:`, dataToSave); - - // Construct the nested update structure for the new API - const nestedUpdate = buildNestedUpdate(groupName, dataToSave); - console.log(`Nested update structure:`, nestedUpdate); + const nestedUpdate = buildNestedUpdate(groupName, { [key]: value }); const response = await fetch(`${config.baseURL}/api/settings`, { method: 'POST', @@ -572,7 +455,6 @@ const useGenericSettings = ( }); if (response.status === 401) { - console.error('Unauthorized access when saving, logging out'); handleLogout(); return; } @@ -581,10 +463,30 @@ const useGenericSettings = ( throw new Error(`HTTP error! status: ${response.status}`); } - console.log(`${groupName} settings saved successfully`); - - // Optionally refresh settings after save to get any server-side changes await fetchSettings(); + } catch (error) { + setError(error instanceof Error ? error : new Error(String(error))); + throw error; + } finally { + setLoading(false); + } + }, [groupName, token, handleLogout, fetchSettings]); + + const saveSettings = useCallback(async () => { + if (!settings) { + console.warn('No settings to save'); + return; + } + + try { + setLoading(true); + setError(null); + + for (const [key, value] of Object.entries(settings)) { + await updateSetting(key, value); + } + + console.log(`${groupName} settings saved successfully`); } catch (error) { console.error(`Error saving ${groupName} settings:`, error); setError(error instanceof Error ? error : new Error(String(error))); @@ -592,7 +494,7 @@ const useGenericSettings = ( } finally { setLoading(false); } - }, [groupName, settings, token, handleLogout, fetchSettings]); + }, [groupName, settings, updateSetting]); // Fetch settings on mount useEffect(() => { @@ -606,6 +508,7 @@ const useGenericSettings = ( fetchSettings, updateSettings, saveSettings, + updateSetting, }; };