Skip to content

Commit d94c012

Browse files
committed
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.
1 parent 81de57c commit d94c012

File tree

1 file changed

+28
-125
lines changed

1 file changed

+28
-125
lines changed

src/hooks/useGenericSettings.ts

Lines changed: 28 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ interface UseGenericSettingsResult<T> {
346346
fetchSettings: () => Promise<void>;
347347
updateSettings: (updatedSettings: Partial<T>) => void;
348348
saveSettings: () => Promise<void>;
349+
updateSetting: (key: string, value: any) => Promise<void>;
349350
}
350351

351352
/**
@@ -437,130 +438,12 @@ const useGenericSettings = <T extends SettingsGroupName>(
437438
});
438439
}, [groupName]);
439440

440-
const saveSettings = useCallback(async () => {
441-
if (!settings) {
442-
console.warn('No settings to save');
443-
return;
444-
}
445-
441+
const updateSetting = useCallback(async (key: string, value: any) => {
446442
try {
447443
setLoading(true);
448444
setError(null);
449445

450-
// By default, use settings as-is
451-
let dataToSave = settings;
452-
453-
// Define settings groups that need special handling
454-
const prefixedSettingsMap: Record<string, { prefix: string, formKeys: string[] }> = {
455-
'image_moderation': {
456-
prefix: 'image_moderation_',
457-
formKeys: [
458-
'image_moderation_api',
459-
'image_moderation_check_interval',
460-
'image_moderation_concurrency',
461-
'image_moderation_enabled',
462-
'image_moderation_mode',
463-
'image_moderation_temp_dir',
464-
'image_moderation_threshold',
465-
'image_moderation_timeout'
466-
]
467-
},
468-
'content_filter': {
469-
prefix: 'content_filter_',
470-
formKeys: [
471-
'content_filter_cache_size',
472-
'content_filter_cache_ttl',
473-
'content_filter_enabled',
474-
'full_text_kinds' // Special case without prefix
475-
]
476-
},
477-
'ollama': {
478-
prefix: 'ollama_',
479-
formKeys: [
480-
'ollama_model',
481-
'ollama_timeout',
482-
'ollama_url'
483-
]
484-
},
485-
'xnostr': {
486-
prefix: 'xnostr_',
487-
formKeys: [
488-
'xnostr_browser_path',
489-
'xnostr_browser_pool_size',
490-
'xnostr_check_interval',
491-
'xnostr_concurrency',
492-
'xnostr_enabled',
493-
'xnostr_temp_dir',
494-
'xnostr_update_interval',
495-
'xnostr_nitter',
496-
'xnostr_verification_intervals'
497-
]
498-
},
499-
'wallet': {
500-
prefix: 'wallet_',
501-
formKeys: [
502-
'wallet_api_key',
503-
'wallet_name'
504-
]
505-
}
506-
};
507-
508-
// Check if this group needs special handling
509-
if (groupName in prefixedSettingsMap) {
510-
console.log(`Settings from state for ${groupName}:`, settings);
511-
const { prefix, formKeys } = prefixedSettingsMap[groupName];
512-
513-
// First fetch complete settings structure to preserve all values
514-
console.log(`Fetching complete settings before saving ${groupName}...`);
515-
const fetchResponse = await fetch(`${config.baseURL}/api/settings`, {
516-
headers: {
517-
'Authorization': `Bearer ${token}`,
518-
},
519-
});
520-
521-
if (!fetchResponse.ok) {
522-
throw new Error(`Failed to fetch current settings: ${fetchResponse.status}`);
523-
}
524-
525-
const currentData = await fetchResponse.json();
526-
const currentSettings = extractSettingsForGroup(currentData.settings, groupName) || {};
527-
console.log(`Current ${groupName} settings from API:`, currentSettings);
528-
529-
// Create a properly prefixed object for the API
530-
const prefixedSettings: Record<string, any> = {};
531-
532-
// Copy all existing settings from the backend with correct prefixes
533-
Object.entries(currentSettings).forEach(([key, value]) => {
534-
// Special case for content_filter's full_text_kinds which doesn't have prefix
535-
if (groupName === 'content_filter' && key === 'full_text_kinds') {
536-
prefixedSettings[key] = value;
537-
} else {
538-
// Skip prefixing if key already has the prefix to avoid double-prefixing
539-
const prefixedKey = key.startsWith(prefix) ? key : `${prefix}${key}`;
540-
prefixedSettings[prefixedKey] = value;
541-
}
542-
});
543-
544-
// Update with changed values from the form
545-
const settingsObj = settings as Record<string, any>;
546-
547-
// Update each field that has changed
548-
formKeys.forEach(formKey => {
549-
if (formKey in settingsObj && settingsObj[formKey] !== undefined) {
550-
console.log(`Updating field: ${formKey} from ${prefixedSettings[formKey]} to ${settingsObj[formKey]}`);
551-
prefixedSettings[formKey] = settingsObj[formKey];
552-
}
553-
});
554-
555-
console.log(`Final ${groupName} settings with prefixed keys for API:`, prefixedSettings);
556-
dataToSave = prefixedSettings as unknown as SettingsGroupType<T>;
557-
}
558-
559-
console.log(`Saving ${groupName} settings:`, dataToSave);
560-
561-
// Construct the nested update structure for the new API
562-
const nestedUpdate = buildNestedUpdate(groupName, dataToSave);
563-
console.log(`Nested update structure:`, nestedUpdate);
446+
const nestedUpdate = buildNestedUpdate(groupName, { [key]: value });
564447

565448
const response = await fetch(`${config.baseURL}/api/settings`, {
566449
method: 'POST',
@@ -572,7 +455,6 @@ const useGenericSettings = <T extends SettingsGroupName>(
572455
});
573456

574457
if (response.status === 401) {
575-
console.error('Unauthorized access when saving, logging out');
576458
handleLogout();
577459
return;
578460
}
@@ -581,18 +463,38 @@ const useGenericSettings = <T extends SettingsGroupName>(
581463
throw new Error(`HTTP error! status: ${response.status}`);
582464
}
583465

584-
console.log(`${groupName} settings saved successfully`);
585-
586-
// Optionally refresh settings after save to get any server-side changes
587466
await fetchSettings();
467+
} catch (error) {
468+
setError(error instanceof Error ? error : new Error(String(error)));
469+
throw error;
470+
} finally {
471+
setLoading(false);
472+
}
473+
}, [groupName, token, handleLogout, fetchSettings]);
474+
475+
const saveSettings = useCallback(async () => {
476+
if (!settings) {
477+
console.warn('No settings to save');
478+
return;
479+
}
480+
481+
try {
482+
setLoading(true);
483+
setError(null);
484+
485+
for (const [key, value] of Object.entries(settings)) {
486+
await updateSetting(key, value);
487+
}
488+
489+
console.log(`${groupName} settings saved successfully`);
588490
} catch (error) {
589491
console.error(`Error saving ${groupName} settings:`, error);
590492
setError(error instanceof Error ? error : new Error(String(error)));
591493
throw error;
592494
} finally {
593495
setLoading(false);
594496
}
595-
}, [groupName, settings, token, handleLogout, fetchSettings]);
497+
}, [groupName, settings, updateSetting]);
596498

597499
// Fetch settings on mount
598500
useEffect(() => {
@@ -606,6 +508,7 @@ const useGenericSettings = <T extends SettingsGroupName>(
606508
fetchSettings,
607509
updateSettings,
608510
saveSettings,
511+
updateSetting,
609512
};
610513
};
611514

0 commit comments

Comments
 (0)