diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index d24473c3..4dc7d300 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -853,6 +853,9 @@ class MetadataArchiveHandler: class FileSystemHandler: + def __init__(self, settings_service=None) -> None: + self._settings = settings_service or get_settings_manager() + async def open_file_location(self, request: web.Request) -> web.Response: try: data = await request.json() @@ -877,6 +880,30 @@ class FileSystemHandler: logger.error("Failed to open file location: %s", exc, exc_info=True) return web.json_response({"success": False, "error": str(exc)}, status=500) + async def open_settings_location(self, request: web.Request) -> web.Response: + try: + settings_file = getattr(self._settings, "settings_file", None) + if not settings_file: + return web.json_response({"success": False, "error": "Settings file not found"}, status=404) + + settings_file = os.path.abspath(settings_file) + if not os.path.isfile(settings_file): + return web.json_response({"success": False, "error": "Settings file does not exist"}, status=404) + + if os.name == "nt": + subprocess.Popen(["explorer", "/select,", settings_file]) + elif os.name == "posix": + if sys.platform == "darwin": + subprocess.Popen(["open", "-R", settings_file]) + else: + folder = os.path.dirname(settings_file) + subprocess.Popen(["xdg-open", folder]) + + return web.json_response({"success": True, "message": f"Opened settings folder: {settings_file}"}) + except Exception as exc: # pragma: no cover - defensive logging + logger.error("Failed to open settings location: %s", exc, exc_info=True) + return web.json_response({"success": False, "error": str(exc)}, status=500) + class NodeRegistryHandler: def __init__( @@ -1103,6 +1130,7 @@ class MiscHandlerSet: "get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status, "get_model_versions_status": self.model_library.get_model_versions_status, "open_file_location": self.filesystem.open_file_location, + "open_settings_location": self.filesystem.open_settings_location, } diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py index a68aa8eb..1a21f57b 100644 --- a/py/routes/misc_route_registrar.py +++ b/py/routes/misc_route_registrar.py @@ -41,6 +41,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = ( RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"), RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"), RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"), + RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"), ) diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 685821b0..89e1e2b9 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -107,7 +107,7 @@ class MiscRoutes: settings_service=self._settings, metadata_provider_updater=self._metadata_provider_updater, ) - filesystem = FileSystemHandler() + filesystem = FileSystemHandler(settings_service=self._settings) node_registry_handler = NodeRegistryHandler( node_registry=self._node_registry, prompt_server=self._prompt_server, diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 908013db..1ad53389 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -883,6 +883,7 @@ class SettingsManager: if os.path.abspath(previous_path) != os.path.abspath(target_path): self._copy_model_cache_directory(previous_dir, target_dir) + logger.info("Switching settings file to: %s", target_path) self._pending_portable_switch = {"other_path": other_path} self.settings_file = target_path @@ -929,7 +930,12 @@ class SettingsManager: and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir) ): try: - shutil.copytree(source_cache_dir, target_cache_dir, dirs_exist_ok=True) + shutil.copytree( + source_cache_dir, + target_cache_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"), + ) except Exception as exc: logger.warning( "Failed to copy model_cache directory from %s to %s: %s", diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 2a7aa6b1..63d2b112 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -17,9 +17,8 @@ export class SettingsManager { this.initializationPromise = null; this.availableLibraries = {}; this.activeLibrary = ''; - this.settingsFilePath = null; this.registeredStartupBannerIds = new Set(); - + // Add initialization to sync with modal state this.currentPage = document.body.dataset.page || 'loras'; @@ -56,7 +55,6 @@ export class SettingsManager { const data = await response.json(); if (data.success && data.settings) { state.global.settings = this.mergeSettingsWithDefaults(data.settings); - this.settingsFilePath = data.settings.settings_file || this.settingsFilePath; this.registerStartupMessages(data.messages); console.log('Settings synced from backend'); } else { @@ -177,10 +175,6 @@ export class SettingsManager { return; } - if (!this.settingsFilePath && typeof message.settings_file === 'string') { - this.settingsFilePath = message.settings_file; - } - const bannerId = `startup-${message.code || index}`; if (this.registeredStartupBannerIds.has(bannerId)) { return; @@ -289,7 +283,7 @@ export class SettingsManager { initialize() { if (this.initialized) return; - + // Add event listener to sync state when modal is closed via other means (like Escape key) const settingsModal = document.getElementById('settingsModal'); if (settingsModal) { @@ -297,7 +291,7 @@ export class SettingsManager { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { this.isOpen = settingsModal.style.display === 'block'; - + // When modal is opened, update checkbox state from current settings if (this.isOpen) { this.loadSettingsToUI(); @@ -305,10 +299,10 @@ export class SettingsManager { } }); }); - + observer.observe(settingsModal, { attributes: true }); } - + // Add event listeners for all toggle-visibility buttons document.querySelectorAll('.toggle-visibility').forEach(button => { button.addEventListener('click', () => this.toggleInputVisibility(button)); @@ -316,12 +310,8 @@ export class SettingsManager { const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger'); if (openSettingsLocationButton) { - if (openSettingsLocationButton.dataset.settingsPath) { - this.settingsFilePath = openSettingsLocationButton.dataset.settingsPath; - } openSettingsLocationButton.addEventListener('click', () => { - const filePath = openSettingsLocationButton.dataset.settingsPath; - this.openSettingsFileLocation(filePath); + this.openSettingsFileLocation(); }); } @@ -364,29 +354,16 @@ export class SettingsManager { this.initialized = true; } - async openSettingsFileLocation(filePath) { - const targetPath = filePath || this.settingsFilePath || document.querySelector('.settings-open-location-trigger')?.dataset.settingsPath; - - if (!targetPath) { - showToast('settings.openSettingsFileLocation.failed', {}, 'error'); - return; - } - + async openSettingsFileLocation() { try { - const response = await fetch('/api/lm/open-file-location', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ file_path: targetPath }), + const response = await fetch('/api/lm/settings/open-location', { + method: 'POST' }); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } - this.settingsFilePath = targetPath; - showToast('settings.openSettingsFileLocation.success', {}, 'success'); } catch (error) { console.error('Failed to open settings file location:', error); @@ -497,7 +474,7 @@ export class SettingsManager { // Load default lora root await this.loadLoraRoots(); - + // Load default checkpoint root await this.loadCheckpointRoots(); @@ -658,7 +635,7 @@ export class SettingsManager { const proxyEnabledCheckbox = document.getElementById('proxyEnabled'); if (proxyEnabledCheckbox) { proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false; - + // Add event listener for toggling proxy settings group visibility proxyEnabledCheckbox.addEventListener('change', () => { const proxySettingsGroup = document.getElementById('proxySettingsGroup'); @@ -666,7 +643,7 @@ export class SettingsManager { proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none'; } }); - + // Set initial visibility const proxySettingsGroup = document.getElementById('proxySettingsGroup'); if (proxySettingsGroup) { @@ -854,23 +831,23 @@ export class SettingsManager { try { const defaultLoraRootSelect = document.getElementById('defaultLoraRoot'); if (!defaultLoraRootSelect) return; - + // Fetch lora roots const response = await fetch('/api/lm/loras/roots'); if (!response.ok) { throw new Error('Failed to fetch LoRA roots'); } - + const data = await response.json(); if (!data.roots || data.roots.length === 0) { throw new Error('No LoRA roots found'); } - + // Clear existing options except the first one (No Default) const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]'); defaultLoraRootSelect.innerHTML = ''; defaultLoraRootSelect.appendChild(noDefaultOption); - + // Add options for each root data.roots.forEach(root => { const option = document.createElement('option'); @@ -878,11 +855,11 @@ export class SettingsManager { option.textContent = root; defaultLoraRootSelect.appendChild(option); }); - + // Set selected value from settings const defaultRoot = state.global.settings.default_lora_root || ''; defaultLoraRootSelect.value = defaultRoot; - + } catch (error) { console.error('Error loading LoRA roots:', error); showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error'); @@ -893,23 +870,23 @@ export class SettingsManager { try { const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot'); if (!defaultCheckpointRootSelect) return; - + // Fetch checkpoint roots const response = await fetch('/api/lm/checkpoints/roots'); if (!response.ok) { throw new Error('Failed to fetch checkpoint roots'); } - + const data = await response.json(); if (!data.roots || data.roots.length === 0) { throw new Error('No checkpoint roots found'); } - + // Clear existing options except the first one (No Default) const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]'); defaultCheckpointRootSelect.innerHTML = ''; defaultCheckpointRootSelect.appendChild(noDefaultOption); - + // Add options for each root data.roots.forEach(root => { const option = document.createElement('option'); @@ -917,11 +894,11 @@ export class SettingsManager { option.textContent = root; defaultCheckpointRootSelect.appendChild(option); }); - + // Set selected value from settings const defaultRoot = state.global.settings.default_checkpoint_root || ''; defaultCheckpointRootSelect.value = defaultRoot; - + } catch (error) { console.error('Error loading checkpoint roots:', error); showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error'); @@ -972,7 +949,7 @@ export class SettingsManager { if (!mappingsContainer) return; const mappings = state.global.settings.base_model_path_mappings || {}; - + // Clear existing mappings mappingsContainer.innerHTML = ''; @@ -993,7 +970,7 @@ export class SettingsManager { const row = document.createElement('div'); row.className = 'mapping-row'; - + const availableModels = MAPPABLE_BASE_MODELS.filter(model => { const existingMappings = state.global.settings.base_model_path_mappings || {}; return !existingMappings.hasOwnProperty(model) || model === baseModel; @@ -1003,9 +980,9 @@ export class SettingsManager {