refactor(settings): invert sync logic from whitelist to blacklist

Replace _SYNC_KEYS (37 keys) with _NO_SYNC_KEYS (5 keys) in SettingsHandler.
New settings automatically sync to frontend unless explicitly excluded.

Changes:
- SettingsHandler now syncs all settings except those in _NO_SYNC_KEYS
- Added keys() method to SettingsManager for iteration
- Updated tests to use new behavior

Benefits:
- No more missing keys when adding new settings
- Reduced maintenance burden
- Explicit exclusions for sensitive/internal settings only

Fixes: #86
This commit is contained in:
Will Miao
2026-02-20 12:14:50 +08:00
parent 1725558fbc
commit 879588e252
5 changed files with 42 additions and 46 deletions

View File

@@ -220,45 +220,17 @@ class HealthCheckHandler:
class SettingsHandler: class SettingsHandler:
"""Sync settings between backend and frontend.""" """Sync settings between backend and frontend."""
_SYNC_KEYS = ( # Settings keys that should NOT be synced to frontend.
"civitai_api_key", # All other settings are synced by default.
"default_lora_root", _NO_SYNC_KEYS = frozenset({
"default_checkpoint_root", # Internal/performance settings (not used by frontend)
"default_unet_root", "hash_chunk_size_mb",
"default_embedding_root", "download_stall_timeout_seconds",
"base_model_path_mappings", # Complex internal structures retrieved via separate endpoints
"download_path_templates", "folder_paths",
"enable_metadata_archive_db", "libraries",
"language", "active_library",
"use_portable_settings", })
"onboarding_completed",
"dismissed_banners",
"proxy_enabled",
"proxy_type",
"proxy_host",
"proxy_port",
"proxy_username",
"proxy_password",
"example_images_path",
"optimize_example_images",
"auto_download_example_images",
"blur_mature_content",
"autoplay_on_hover",
"display_density",
"card_info_display",
"show_folder_sidebar",
"include_trigger_words",
"show_only_sfw",
"compact_mode",
"priority_tags",
"model_card_footer_action",
"model_name_display",
"update_flag_strategy",
"auto_organize_exclusions",
"metadata_refresh_skip_paths",
"filter_presets",
"hide_early_access_updates",
)
_PROXY_KEYS = { _PROXY_KEYS = {
"proxy_enabled", "proxy_enabled",
@@ -305,10 +277,12 @@ class SettingsHandler:
async def get_settings(self, request: web.Request) -> web.Response: async def get_settings(self, request: web.Request) -> web.Response:
try: try:
response_data = {} response_data = {}
for key in self._SYNC_KEYS: # Sync all settings except those in _NO_SYNC_KEYS
value = self._settings.get(key) for key in self._settings.keys():
if value is not None: if key not in self._NO_SYNC_KEYS:
response_data[key] = value value = self._settings.get(key)
if value is not None:
response_data[key] = value
settings_file = getattr(self._settings, "settings_file", None) settings_file = getattr(self._settings, "settings_file", None)
if settings_file: if settings_file:
response_data["settings_file"] = settings_file response_data["settings_file"] = settings_file

View File

@@ -994,6 +994,10 @@ class SettingsManager:
self._save_settings() self._save_settings()
logger.info(f"Deleted setting: {key}") logger.info(f"Deleted setting: {key}")
def keys(self) -> Iterable[str]:
"""Return all setting keys."""
return self.settings.keys()
def _prepare_portable_switch(self, use_portable: bool) -> None: def _prepare_portable_switch(self, use_portable: bool) -> None:
"""Prepare switching the settings storage location.""" """Prepare switching the settings storage location."""

View File

@@ -26,6 +26,7 @@
'settings': dict({ 'settings': dict({
'civitai_api_key': 'test-key', 'civitai_api_key': 'test-key',
'language': 'en', 'language': 'en',
'theme': 'dark',
}), }),
'success': True, 'success': True,
}) })

View File

@@ -45,6 +45,9 @@ class DummySettings:
def set(self, key, value): def set(self, key, value):
self.data[key] = value self.data[key] = value
def keys(self):
return self.data.keys()
async def noop_async(*_args, **_kwargs): async def noop_async(*_args, **_kwargs):
"""No-op async function.""" """No-op async function."""

View File

@@ -44,6 +44,9 @@ class DummySettings:
def delete(self, key): def delete(self, key):
self.data.pop(key, None) self.data.pop(key, None)
def keys(self):
return self.data.keys()
class DummyDownloader: class DummyDownloader:
def __init__(self): def __init__(self):
@@ -62,8 +65,14 @@ async def dummy_downloader_factory():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_settings_filters_sync_keys(): async def test_get_settings_excludes_no_sync_keys():
settings_service = DummySettings({"civitai_api_key": "abc", "extraneous": "value"}) """Verify that settings in _NO_SYNC_KEYS are not synced, but others are."""
settings_service = DummySettings({
"civitai_api_key": "abc",
"hash_chunk_size_mb": 10,
"folder_paths": {"/some/path"},
"regular_setting": "value",
})
handler = SettingsHandler( handler = SettingsHandler(
settings_service=settings_service, settings_service=settings_service,
metadata_provider_updater=noop_async, metadata_provider_updater=noop_async,
@@ -74,7 +83,12 @@ async def test_get_settings_filters_sync_keys():
payload = json.loads(response.text) payload = json.loads(response.text)
assert payload["success"] is True assert payload["success"] is True
assert payload["settings"] == {"civitai_api_key": "abc"} # Regular settings should be synced
assert payload["settings"]["civitai_api_key"] == "abc"
assert payload["settings"]["regular_setting"] == "value"
# _NO_SYNC_KEYS should not be synced
assert "hash_chunk_size_mb" not in payload["settings"]
assert "folder_paths" not in payload["settings"]
@pytest.mark.asyncio @pytest.mark.asyncio