import { getSvgIcon } from "../../utils/icon_utils.js"; import { logger as frontendLogger } from "../../log_system/logger.js"; import { escapeHtml, pollBackgroundTask, safeStorage } from "../utils/html_utils.js"; const SETTINGS_MAP = []; export const optionsMethods = { confirmDiscardOptionsChanges() { if (this.activeTab !== 'options') return true; const status = this.contentElement?.querySelector('#mr-options-status'); if (!status?.classList.contains('is-dirty')) return true; return window.confirm('You have unsaved options. Are you sure you want to leave without saving?'); }, requestClose(...args) { if (!this.confirmDiscardOptionsChanges()) return false; return this.close(...args); }, buildLocalModelContext(item = {}, contextSource = '', extraFields = {}) { const modelLabel = item.relative_path || item.filename || item.model_path || 'Model'; const modelPath = item.model_path || ''; if (!modelPath) return null; return { context_scope: 'local_model', open_folder_label: 'Show File in Folder', name: item.filename || modelLabel, filename: item.filename || modelLabel, relative_path: item.relative_path || modelLabel, path: modelPath, resolved_path: modelPath, open_path: modelPath, folder_path: item.base_directory || '', category: item.category || '', metadata_path: item.metadata_path || '', ...extraFields, context_source: contextSource }; }, displayOptions() { if (!this.contentElement) return; this.contentElement.style.overflowY = 'hidden'; const tokens = this.getStoredTokens(); const pathTemplateCategories = this.getDownloadPathTemplateCategoryDefinitions(); const defaultRootCategories = this.getDefaultRootCategoryDefinitions(); const pathTemplatePresets = this.getDownloadPathTemplatePresetDefinitions(); const pathTemplatePresetValues = new Set(pathTemplatePresets.map(preset => preset.value)); const renderPathModeOptions = (selectedValue) => { const selected = this.normalizeDownloadPathMode(selectedValue); const options = [ { value: 'suggested', label: 'Suggest existing folders', tooltip: 'Uses current Model Resolver behavior: it fills an existing matching folder when possible.' }, { value: 'template', label: 'Use path templates', tooltip: 'Creates subfolders from template placeholders such as base model, author, tags, and model name.' }, { value: 'manual', label: 'Manual only', tooltip: 'Leaves the subfolder field empty until you type or pick a folder.' } ]; return options .map(option => ``) .join(''); }; const renderDownloadBackendOptions = (selectedValue) => { const selected = this.normalizeDownloadBackend(selectedValue); const options = [ { value: 'python', label: 'Python (built-in)', tooltip: 'Uses Model Resolver built-in downloader. This remains the default and requires no external tools.' }, { value: 'aria2', label: 'aria2 (experimental)', tooltip: 'Uses an external aria2c process for large downloads, pause/resume, and segmented transfers.' } ]; return options .map(option => ``) .join(''); }; const renderTemplatePlaceholderBadges = () => ['{base_model}', '{author}', '{first_tag}', '{model_name}', '{version_name}'] .map(placeholder => ``) .join(''); const renderPathTemplateRows = () => pathTemplateCategories .map(category => { const template = tokens.download_path_templates?.[category.key] ?? this.getDefaultDownloadPathTemplates()[category.key] ?? ''; const normalizedTemplate = this.normalizeDownloadPathValue(template); const isPreset = pathTemplatePresetValues.has(normalizedTemplate); const selectedValue = isPreset ? normalizedTemplate : 'custom'; const options = [ ...pathTemplatePresets.map(preset => ( `` )), `` ].join(''); const preview = this.calculateDownloadPathTemplateSubfolder(category.key, { base_model: 'Flux.1 D', tags: ['style', 'character'], author: 'example_author', model_name: 'Example Model', version_name: 'v1' }) || '(flat folder)'; return `
`; }) .join(''); const renderDefaultRootRows = () => defaultRootCategories .map(category => { const value = tokens[category.settingKey] || ''; return ` `; }) .join(''); const sourceDefaultsRows = this.getSearchSourceDefinitions() .filter(def => this.isSourceAvailable(def.source)) .map(def => { const iconName = this.getSearchSourceIconName(def.source); const checked = tokens.search_source_enabled?.[def.source] !== false; const inputId = `mr-options-source-${def.source.replace(/_/g, '-')}`; return ` `; }) .join(''); const logLevelValues = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'NONE']; const normalizeLogLevel = (value) => { const normalized = String(value || '').trim().toUpperCase(); return logLevelValues.includes(normalized) ? normalized : 'DEBUG'; }; const renderLogLevelOptions = (selectedValue) => { const selected = normalizeLogLevel(selectedValue); return logLevelValues .map(level => ``) .join(''); }; this.contentElement.innerHTML = ` `; const civitaiInput = this.contentElement.querySelector('#mr-options-civitai'); const civitaiSessionInput = this.contentElement.querySelector('#mr-options-civitai-session'); const hfInput = this.contentElement.querySelector('#mr-options-hf'); const braveInput = this.contentElement.querySelector('#mr-options-brave'); const civitaiToggle = this.contentElement.querySelector('#mr-options-civitai-toggle'); const civitaiSessionToggle = this.contentElement.querySelector('#mr-options-civitai-session-toggle'); const hfToggle = this.contentElement.querySelector('#mr-options-hf-toggle'); const braveToggle = this.contentElement.querySelector('#mr-options-brave-toggle'); const civitaiLimitInput = this.contentElement.querySelector('#mr-options-civitai-limit'); const civitaiUseTrpcSearchInput = this.contentElement.querySelector('#mr-options-civitai-use-trpc-search'); const civitaiUseApiSearchInput = this.contentElement.querySelector('#mr-options-civitai-use-api-search'); const civitaiUseHtmlFallbackInput = this.contentElement.querySelector('#mr-options-civitai-use-html-fallback'); const civitaiKeyCheckBtn = this.contentElement.querySelector('#mr-options-civitai-key-check'); const civitaiKeyCheckStatus = this.contentElement.querySelector('#mr-options-civitai-key-check-status'); const civitaiSessionCheckBtn = this.contentElement.querySelector('#mr-options-civitai-session-check'); const civitaiSessionCheckStatus = this.contentElement.querySelector('#mr-options-civitai-session-check-status'); const hfUseApiSearchInput = this.contentElement.querySelector('#mr-options-hf-use-api-search'); const hfUseComfyOrgFallbackInput = this.contentElement.querySelector('#mr-options-hf-use-comfy-org-fallback'); const hfUseBraveFallbackInput = this.contentElement.querySelector('#mr-options-hf-use-brave-fallback'); const autoFillBaseModelInput = this.contentElement.querySelector('#mr-options-auto-fill-base-model'); const autoFillSubfolderInput = this.contentElement.querySelector('#mr-options-auto-fill-subfolder'); const autoRefreshComfyModelsInput = this.contentElement.querySelector('#mr-options-auto-refresh-comfy-models'); const autoOpenOnMissingInput = this.contentElement.querySelector('#mr-options-auto-open-on-missing'); const workflowHashMetadataInput = this.contentElement.querySelector('#mr-options-workflow-hash-metadata'); const workflowDependencyMarkerInput = this.contentElement.querySelector('#mr-options-workflow-dependency-marker'); const addDependencyNodeBtn = this.contentElement.querySelector('#mr-options-add-dependency-node'); const downloadBackendInput = this.contentElement.querySelector('#mr-options-download-backend'); const aria2cPathInput = this.contentElement.querySelector('#mr-options-aria2c-path'); const aria2InstallBtn = this.contentElement.querySelector('#mr-options-aria2-install'); const aria2CheckBtn = this.contentElement.querySelector('#mr-options-aria2-check'); const aria2StopBtn = this.contentElement.querySelector('#mr-options-aria2-stop'); const aria2StatusEl = this.contentElement.querySelector('#mr-options-aria2-status'); const aria2BackendStateEl = this.contentElement.querySelector('#mr-options-aria2-backend-state'); const aria2AvailabilityEl = this.contentElement.querySelector('#mr-options-aria2-availability'); const aria2DaemonEl = this.contentElement.querySelector('#mr-options-aria2-daemon'); const aria2AutoStopInput = this.contentElement.querySelector('#mr-options-aria2-auto-stop'); const downloadSoundInput = this.contentElement.querySelector('#mr-options-download-sound'); const aria2SettingEls = Array.from(this.contentElement.querySelectorAll('.mr-options-aria2-setting')); const downloadPathModeInput = this.contentElement.querySelector('#mr-options-download-path-mode'); const defaultRootSelectInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-default-root')); const templatePresetInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-template-preset')); const templateCustomInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-template-custom')); const detectTemplatesBtn = this.contentElement.querySelector('#mr-options-detect-templates'); const templateDetectStatus = this.contentElement.querySelector('#mr-options-template-detect-status'); const baseModelMappingsContainer = this.contentElement.querySelector('#mr-options-base-model-mappings'); const addBaseModelMappingBtn = this.contentElement.querySelector('#mr-options-add-base-model-mapping'); const frontendLogsEnabledInput = this.contentElement.querySelector('#mr-options-frontend-logs-enabled'); const backendLogsEnabledInput = this.contentElement.querySelector('#mr-options-backend-logs-enabled'); const frontendLogLevelInput = this.contentElement.querySelector('#mr-options-frontend-log-level'); const backendLogLevelInput = this.contentElement.querySelector('#mr-options-backend-log-level'); const exportFrontendLogsBtn = this.contentElement.querySelector('#mr-options-export-frontend-logs'); const exportBackendLogsBtn = this.contentElement.querySelector('#mr-options-export-backend-logs'); const exportFrontendLogsStatus = this.contentElement.querySelector('#mr-options-export-frontend-logs-status'); const exportBackendLogsStatus = this.contentElement.querySelector('#mr-options-export-backend-logs-status'); const hfTokenCheckBtn = this.contentElement.querySelector('#mr-options-hf-token-check'); const hfTokenCheckStatus = this.contentElement.querySelector('#mr-options-hf-token-check-status'); const braveKeyCheckBtn = this.contentElement.querySelector('#mr-options-brave-key-check'); const braveKeyCheckStatus = this.contentElement.querySelector('#mr-options-brave-key-check-status'); const sourceEnabledInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-source-enabled')); const status = this.contentElement.querySelector('#mr-options-status'); const saveBtn = this.contentElement.querySelector('#mr-options-save'); const modelListCountEl = this.contentElement.querySelector('#mr-options-model-list-count'); const modelListUpdatedEl = this.contentElement.querySelector('#mr-options-model-list-updated'); const modelListStateEl = this.contentElement.querySelector('#mr-options-model-list-state'); const modelListMessageEl = this.contentElement.querySelector('#mr-options-model-list-message'); const modelListCheckBtn = this.contentElement.querySelector('#mr-options-model-list-check'); const modelListUpdateBtn = this.contentElement.querySelector('#mr-options-model-list-update'); const baseModelsCountEl = this.contentElement.querySelector('#mr-options-base-models-count'); const baseModelsUpdatedEl = this.contentElement.querySelector('#mr-options-base-models-updated'); const baseModelsStateEl = this.contentElement.querySelector('#mr-options-base-models-state'); const baseModelsMessageEl = this.contentElement.querySelector('#mr-options-base-models-message'); const baseModelsCheckBtn = this.contentElement.querySelector('#mr-options-base-models-check'); const baseModelsUpdateBtn = this.contentElement.querySelector('#mr-options-base-models-update'); const hfIndexAuthorsEl = this.contentElement.querySelector('#mr-options-hf-index-authors'); const hfIndexUpdatedEl = this.contentElement.querySelector('#mr-options-hf-index-updated'); const hfIndexStateEl = this.contentElement.querySelector('#mr-options-hf-index-state'); const hfIndexMessageEl = this.contentElement.querySelector('#mr-options-hf-index-message'); const hfIndexRefreshBtn = this.contentElement.querySelector('#mr-options-hf-index-refresh'); const clearAllCacheBtn = this.contentElement.querySelector('#mr-options-clear-all-cache'); const clearAllCacheStatus = this.contentElement.querySelector('#mr-options-clear-cache-status'); const metadataSizeAuditBtn = this.contentElement.querySelector('#mr-options-metadata-size-scan'); const metadataSizeAuditStatus = this.contentElement.querySelector('#mr-options-metadata-size-status'); const metadataSizeAuditResults = this.contentElement.querySelector('#mr-options-metadata-size-results'); const metadataSizeScannedEl = this.contentElement.querySelector('#mr-options-metadata-size-scanned'); const metadataSizeCheckedEl = this.contentElement.querySelector('#mr-options-metadata-size-checked'); const metadataSizeMismatchesEl = this.contentElement.querySelector('#mr-options-metadata-size-mismatches'); const metadataSizeMissingEl = this.contentElement.querySelector('#mr-options-metadata-size-missing'); const metadataSizeErrorsEl = this.contentElement.querySelector('#mr-options-metadata-size-errors'); const metadataSizeCollapseToggle = this.contentElement.querySelector('#mr-options-metadata-size-collapse-toggle'); const metadataSizeControls = this.contentElement.querySelector('#mr-options-metadata-size-controls'); const metadataSizeStack = this.contentElement.querySelector('.mr-options-metadata-size-stack'); const metadataBuildStartBtn = this.contentElement.querySelector('#mr-options-metadata-build-start'); const metadataBuildCancelBtn = this.contentElement.querySelector('#mr-options-metadata-build-cancel'); const metadataBuildStatus = this.contentElement.querySelector('#mr-options-metadata-build-status'); const metadataBuildProgress = this.contentElement.querySelector('#mr-options-metadata-build-progress'); const metadataBuildStageEl = this.contentElement.querySelector('#mr-options-metadata-build-stage'); const metadataBuildPercentEl = this.contentElement.querySelector('#mr-options-metadata-build-percent'); const metadataBuildBarEl = this.contentElement.querySelector('#mr-options-metadata-build-bar'); const metadataBuildCurrentModelEl = this.contentElement.querySelector('#mr-options-metadata-build-current-model'); const metadataBuildCurrentPathEl = this.contentElement.querySelector('#mr-options-metadata-build-current-path'); const metadataBuildBytesEl = this.contentElement.querySelector('#mr-options-metadata-build-bytes'); const metadataBuildWorkerListEl = this.contentElement.querySelector('#mr-options-metadata-build-worker-list'); const metadataBuildResults = this.contentElement.querySelector('#mr-options-metadata-build-results'); const metadataBuildTabButtons = Array.from(this.contentElement.querySelectorAll('.mr-options-build-tab')); const metadataBuildProgressPanel = this.contentElement.querySelector('#mr-options-metadata-build-progress-panel'); const metadataBuildHistoryPanel = this.contentElement.querySelector('#mr-options-metadata-build-history-panel'); const metadataBuildHistoryCountEl = this.contentElement.querySelector('#mr-options-metadata-build-history-count'); const metadataBuildCpuEl = this.contentElement.querySelector('#mr-options-metadata-build-cpu'); const metadataBuildWorkersEl = this.contentElement.querySelector('#mr-options-metadata-build-workers'); const metadataBuildWorkerInput = this.contentElement.querySelector('#mr-options-metadata-build-worker-count'); const metadataBuildWorkerHint = this.contentElement.querySelector('#mr-options-metadata-build-worker-hint'); const metadataBuildModeSelect = this.contentElement.querySelector('#mr-options-metadata-build-mode'); const metadataBuildModeHint = this.contentElement.querySelector('#mr-options-metadata-build-mode-hint'); const metadataBuildScannedEl = this.contentElement.querySelector('#mr-options-metadata-build-scanned'); const metadataBuildCreatedEl = this.contentElement.querySelector('#mr-options-metadata-build-created'); const metadataBuildUpdatedEl = this.contentElement.querySelector('#mr-options-metadata-build-updated'); const metadataBuildSkippedEl = this.contentElement.querySelector('#mr-options-metadata-build-skipped'); const metadataBuildHashesEl = this.contentElement.querySelector('#mr-options-metadata-build-hashes'); const metadataBuildErrorsEl = this.contentElement.querySelector('#mr-options-metadata-build-errors'); const metadataBuildCollapseToggle = this.contentElement.querySelector('#mr-options-metadata-build-collapse-toggle'); const metadataBuildControls = this.contentElement.querySelector('#mr-options-metadata-build-controls'); const metadataBuildStack = this.contentElement.querySelector('.mr-options-build-stack'); const optionsMain = this.contentElement.querySelector('.mr-options-main'); const navButtons = Array.from(this.contentElement.querySelectorAll('.mr-options-nav-btn')); const optionSections = Array.from(this.contentElement.querySelectorAll('.mr-options-section')); const metadataBuildHistoryFilterValues = ['all', 'done', 'created', 'updated', 'skipped', 'checked', 'error', 'cancelled']; if (!metadataBuildHistoryFilterValues.includes(this.metadataBuildHistoryFilter)) { this.metadataBuildHistoryFilter = 'all'; } this.metadataBuildHistoryFilterMenuOpen = false; const metadataBuildHistoryPageSize = 250; if (!Number.isFinite(Number(this.metadataBuildHistoryVisibleLimit)) || Number(this.metadataBuildHistoryVisibleLimit) < metadataBuildHistoryPageSize) { this.metadataBuildHistoryVisibleLimit = metadataBuildHistoryPageSize; } if (this.metadataBuildPollTimer) { window.clearTimeout(this.metadataBuildPollTimer); this.metadataBuildPollTimer = null; } const trackedInputs = [ civitaiInput, civitaiSessionInput, hfInput, braveInput, civitaiLimitInput, civitaiUseTrpcSearchInput, civitaiUseApiSearchInput, civitaiUseHtmlFallbackInput, hfUseApiSearchInput, hfUseComfyOrgFallbackInput, hfUseBraveFallbackInput, autoFillBaseModelInput, autoFillSubfolderInput, autoRefreshComfyModelsInput, autoOpenOnMissingInput, workflowHashMetadataInput, workflowDependencyMarkerInput, downloadBackendInput, aria2cPathInput, aria2AutoStopInput, downloadSoundInput, downloadPathModeInput, ...defaultRootSelectInputs, ...templatePresetInputs, ...templateCustomInputs, frontendLogsEnabledInput, backendLogsEnabledInput, frontendLogLevelInput, backendLogLevelInput, ...sourceEnabledInputs, ].filter(Boolean); const setStatus = (text, mode = '') => { if (!status) return; status.textContent = text; status.classList.remove('is-dirty', 'is-saved'); if (mode) status.classList.add(mode); }; const setExportLogsStatus = (statusEl, text) => { if (statusEl) statusEl.textContent = text; }; const parseDownloadFilename = (header, fallback) => { const value = String(header || ''); const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i); if (utf8Match?.[1]) { try { return decodeURIComponent(utf8Match[1].trim().replace(/^"|"$/g, '')); } catch (_error) { return utf8Match[1].trim().replace(/^"|"$/g, '') || fallback; } } const match = value.match(/filename="?([^";]+)"?/i); return match?.[1]?.trim() || fallback; }; const downloadBlob = (blob, filename) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const exportFrontendLogs = () => { try { if (exportFrontendLogsBtn) exportFrontendLogsBtn.disabled = true; const exported = frontendLogger.exportLogs('txt'); if (!exported) { setExportLogsStatus(exportFrontendLogsStatus, 'No frontend logs captured yet.'); this.showNotification('No frontend logs to export', 'warning'); return; } setExportLogsStatus(exportFrontendLogsStatus, 'Frontend logs exported.'); this.showNotification('Frontend logs exported', 'success'); } catch (error) { console.error('Model Resolver: frontend log export failed:', error); setExportLogsStatus(exportFrontendLogsStatus, error.message || 'Frontend log export failed.'); this.showNotification('Frontend log export failed', 'error'); } finally { if (exportFrontendLogsBtn) exportFrontendLogsBtn.disabled = false; } }; const exportBackendLogs = async () => { try { if (exportBackendLogsBtn) exportBackendLogsBtn.disabled = true; setExportLogsStatus(exportBackendLogsStatus, 'Preparing backend logs...'); const response = await this.fetchJson( '/model_resolver/logs/backend/export', { raw: true }, 'Export backend logs' ); const blob = await response.blob(); const filename = parseDownloadFilename( response.headers.get('Content-Disposition'), `model_resolver_backend_logs_${new Date().toISOString().replace(/[:.]/g, '-')}.txt` ); downloadBlob(blob, filename); setExportLogsStatus(exportBackendLogsStatus, 'Backend logs exported.'); this.showNotification('Backend logs exported', 'success'); } catch (error) { setExportLogsStatus(exportBackendLogsStatus, error.message || 'Backend log export failed.'); } finally { if (exportBackendLogsBtn) exportBackendLogsBtn.disabled = false; } }; const formatOptionDate = (value) => { if (!value) return 'Bundled'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); }; const shortSha = (value) => value ? String(value).slice(0, 8) : ''; const refreshButtons = new Set([ modelListCheckBtn, baseModelsCheckBtn, hfIndexRefreshBtn, metadataSizeAuditBtn, aria2CheckBtn, civitaiKeyCheckBtn, civitaiSessionCheckBtn, hfTokenCheckBtn, braveKeyCheckBtn, clearAllCacheBtn ]); const refreshAnimations = new Map(); const setControlsBusyState = (buttons, busy, cancelBtn = null) => { const btnList = Array.isArray(buttons) ? buttons : [buttons]; btnList.forEach(btn => { if (!btn) return; btn.disabled = busy; if (!refreshButtons.has(btn)) return; refreshAnimations.get(btn)?.cancel(); refreshAnimations.delete(btn); btn.classList.toggle('mr-is-refreshing', busy); if (busy) { refreshAnimations.set(btn, this.startRefreshButtonAnimation(btn)); } }); if (cancelBtn) { cancelBtn.hidden = !busy; cancelBtn.disabled = false; } }; const setModelListBusy = (busy) => setControlsBusyState([modelListCheckBtn, modelListUpdateBtn], busy); const setBaseModelsBusy = (busy) => setControlsBusyState([baseModelsCheckBtn, baseModelsUpdateBtn], busy); const setHfIndexBusy = (busy) => setControlsBusyState(hfIndexRefreshBtn, busy); const setMetadataSizeAuditBusy = (busy) => setControlsBusyState(metadataSizeAuditBtn, busy); const setStatusMode = (element, mode = '') => { if (!element) return; element.classList.remove('is-valid', 'is-invalid', 'is-pending'); if (mode) element.classList.add(mode); }; const formatAuditNumber = (value) => { const number = Number(value || 0); return Number.isFinite(number) ? number.toLocaleString() : '0'; }; const setElementStatusText = (element, text, mode = '') => { if (!element) return; element.textContent = text; setStatusMode(element, mode); }; const setMetadataSizeAuditStatus = (text, mode = '') => setElementStatusText(metadataSizeAuditStatus, text, mode); const setSummaryValue = (element, value, mode = '') => { if (!element) return; element.textContent = formatAuditNumber(value); setStatusMode(element, mode); }; const setMetadataSizeControlsCollapsed = (collapsed, persist = true) => { const isCollapsed = Boolean(collapsed); this.metadataSizeControlsCollapsed = isCollapsed; if (metadataSizeControls) { metadataSizeControls.hidden = isCollapsed; } if (metadataSizeStack) { metadataSizeStack.classList.toggle('is-controls-collapsed', isCollapsed); } if (metadataSizeCollapseToggle) { metadataSizeCollapseToggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true'); this.setTooltip?.( metadataSizeCollapseToggle, isCollapsed ? 'Show metadata size audit controls' : 'Hide metadata size audit controls' ); } if (persist) { try { safeStorage.setItem( this.metadataSizeControlsCollapsedStorageKey || 'model_resolver_metadata_size_controls_collapsed', isCollapsed ? '1' : '0' ); } catch (error) { console.warn('Model Resolver: could not save metadata size audit collapse state:', error); } } }; if (!this.metadataSizeControlsCollapsedStorageKey) { this.metadataSizeControlsCollapsedStorageKey = 'model_resolver_metadata_size_controls_collapsed'; } if (typeof this.metadataSizeControlsCollapsed !== 'boolean') { try { this.metadataSizeControlsCollapsed = safeStorage.getItem(this.metadataSizeControlsCollapsedStorageKey) === '1'; } catch (_error) { this.metadataSizeControlsCollapsed = false; } } setMetadataSizeControlsCollapsed(this.metadataSizeControlsCollapsed, false); const updateMetadataSizeAuditSummary = (data = {}) => { const mismatchCount = Number(data.mismatch_count || 0); const missingSizeCount = Number(data.missing_size || 0); const errorCount = Number(data.error_count || 0); setSummaryValue(metadataSizeScannedEl, data.scanned_models || 0); setSummaryValue(metadataSizeCheckedEl, data.metadata_files ?? data.checked_metadata ?? 0); setSummaryValue(metadataSizeMismatchesEl, mismatchCount, mismatchCount > 0 ? 'is-invalid' : ''); setSummaryValue(metadataSizeMissingEl, missingSizeCount, missingSizeCount > 0 ? 'is-pending' : ''); setSummaryValue(metadataSizeErrorsEl, errorCount, errorCount > 0 ? 'is-invalid' : ''); }; const renderMetadataSizeAuditResults = (data = null) => { if (!metadataSizeAuditResults) return; if (!data) { metadataSizeAuditResults.hidden = true; metadataSizeAuditResults.innerHTML = ''; return; } const mismatches = Array.isArray(data.mismatches) ? data.mismatches : []; const errors = Array.isArray(data.errors) ? data.errors : []; const visibleMismatches = mismatches.slice(0, 500); let html = ''; if (mismatches.length) { const rows = visibleMismatches.map((item) => { const modelLabel = item.relative_path || item.filename || item.model_path || 'Model'; const modelPath = item.model_path || ''; const metadataPath = item.metadata_path || ''; const metadataSize = item.metadata_size_label || this.formatBytes(item.metadata_size); const actualSize = item.actual_size_label || this.formatBytes(item.actual_size); const difference = Number(item.difference || 0); const differenceClass = difference > 0 ? 'is-positive' : (difference < 0 ? 'is-negative' : ''); const differenceLabel = item.difference_label || `${difference >= 0 ? '+' : '-'}${this.formatBytes(Math.abs(difference))}`; const sizeField = item.size_field || 'size'; const auditContext = this.buildLocalModelContext(item, 'metadata_size_audit', { size: item.actual_size || 0, file_size: item.actual_size || 0, metadata_size: item.metadata_size || 0 }); const contextAttrs = auditContext ? this.getContextMenuAttrs(auditContext, 'Right-click for model options') : ''; return `${this.escapeHtml(targetPath)}${this.escapeHtml(targetPath)}${this.escapeHtml(renderTemplatePreview(getTemplateInputValue(categoryKey)))}`;
}
};
const syncAllTemplateControls = () => {
pathTemplateCategories.forEach(category => syncTemplateControl(category.key));
};
const getDefaultRootContextPath = (select) => {
if (!select) return '';
const category = this.normalizeDownloadCategory(select.dataset.rootCategory || '');
const selectedValue = String(select.value || '').trim();
if (selectedValue) return selectedValue;
const directDownloadRoot = this.downloadDirectories?.[category] || '';
if (directDownloadRoot) return directDownloadRoot;
const rootOptions = this.downloadRootDirectories?.[category];
return Array.isArray(rootOptions) && rootOptions.length ? rootOptions[0] : '';
};
const getDefaultRootContextModel = (select) => {
const path = getDefaultRootContextPath(select);
if (!path) return null;
const category = this.normalizeDownloadCategory(select.dataset.rootCategory || '');
const label = select.closest('.mr-options-template-row')?.querySelector('.mr-options-label')?.textContent?.trim()
|| this.getCategoryDisplayName(category);
return {
context_scope: 'download_root',
name: label,
path,
resolved_path: path,
folder_path: path,
download_directory: path,
category,
open_folder_label: 'Open Root Folder'
};
};
const syncDefaultRootContextTarget = (select) => {
if (!select) return;
const contextModel = getDefaultRootContextModel(select);
const tooltip = contextModel
? `Right-click to open ${contextModel.name}`
: 'No root folder available';
this.setDownloadFolderContextTarget(select, contextModel, tooltip);
this.setDownloadFolderContextTarget(select.closest('.mr-options-template-row'), contextModel, tooltip);
};
const setTemplateControlValue = (categoryKey, template) => {
const select = templatePresetInputs.find(input => input.dataset.templateCategory === categoryKey);
const custom = templateCustomInputs.find(input => input.dataset.templateCategory === categoryKey);
if (!select) return false;
const normalizedTemplate = this.normalizeDownloadPathValue(template);
select.value = pathTemplatePresetValues.has(normalizedTemplate)
? normalizedTemplate
: 'custom';
if (custom) {
custom.value = normalizedTemplate;
}
syncTemplateControl(categoryKey);
return true;
};
const setTemplateDetectStatus = (text, mode = '') => {
if (!templateDetectStatus) return;
templateDetectStatus.textContent = text;
templateDetectStatus.classList.remove('is-valid', 'is-invalid', 'is-pending');
if (mode) templateDetectStatus.classList.add(mode);
};
const applyDetectedPathTemplates = async () => {
if (!detectTemplatesBtn) return;
detectTemplatesBtn.disabled = true;
setTemplateDetectStatus('Scanning existing model folders...', 'is-pending');
try {
const data = await this.fetchJson('/model_resolver/path-template-suggestions?force=1', {}, 'Detect path templates');
const categories = data?.categories && typeof data.categories === 'object'
? data.categories
: {};
let appliedCount = 0;
let inspectedCount = 0;
pathTemplateCategories.forEach((category) => {
const suggestion = categories[category.key];
if (!suggestion) return;
if (Number(suggestion.model_count || 0) > 0) {
inspectedCount += 1;
}
if (suggestion.apply && setTemplateControlValue(category.key, suggestion.template || '')) {
appliedCount += 1;
}
});
let mappingCount = 0;
const detectedMappings = data?.base_model_path_mappings || {};
if (
baseModelMappingsContainer &&
detectedMappings &&
typeof detectedMappings === 'object' &&
!Array.isArray(detectedMappings)
) {
const currentMappings = collectBaseModelPathMappings();
Object.entries(detectedMappings).forEach(([source, target]) => {
const sourceKey = String(source || '').trim();
const targetValue = String(target || '').trim();
if (sourceKey && targetValue && !currentMappings[sourceKey]) {
currentMappings[sourceKey] = targetValue;
mappingCount += 1;
}
});
if (mappingCount > 0) {
renderBaseModelMappingRows(currentMappings);
}
}
if (appliedCount > 0 || mappingCount > 0) {
syncAllTemplateControls();
setStatus('You have unsaved changes.', 'is-dirty');
const mappingText = mappingCount
? ` Added ${mappingCount} base-model mapping${mappingCount === 1 ? '' : 's'}.`
: '';
setTemplateDetectStatus(
appliedCount > 0
? `Detected ${appliedCount} template preset${appliedCount === 1 ? '' : 's'} from ${inspectedCount} model type${inspectedCount === 1 ? '' : 's'}.${mappingText}`
: `Detected ${mappingCount} base-model mapping${mappingCount === 1 ? '' : 's'} from existing folders.`,
'is-valid'
);
this.showNotification('Path templates detected. Review and save options.', 'success');
} else {
setTemplateDetectStatus(
'No confident folder pattern found. Existing template settings were not changed.',
'is-invalid'
);
this.showNotification('No confident path template pattern found.', 'info');
}
} catch (error) {
console.error('Model Resolver: path template detection error:', error);
setTemplateDetectStatus(error.message || 'Template detection failed.', 'is-invalid');
this.showNotification('Path template detection failed', 'error');
} finally {
detectTemplatesBtn.disabled = false;
}
};
templatePresetInputs.forEach(input => {
input.addEventListener('change', () => syncTemplateControl(input.dataset.templateCategory));
});
templateCustomInputs.forEach(input => {
input.addEventListener('input', () => syncTemplateControl(input.dataset.templateCategory));
});
renderBaseModelMappingRows(tokens.base_model_path_mappings || {});
if (typeof this.ensureBaseModelsLoaded === 'function') {
this.ensureBaseModelsLoaded()
.then(() => {
refreshBaseModelMappingOptions();
})
.catch((error) => {
console.warn('Model Resolver: could not load base models for mapping options', error);
});
}
if (addBaseModelMappingBtn) {
addBaseModelMappingBtn.addEventListener('click', () => {
addBaseModelMappingRow('', '');
setStatus('You have unsaved changes.', 'is-dirty');
});
}
syncAllTemplateControls();
const populateDefaultRootSelects = async () => {
const roots = await this.ensureDownloadRootDirectoriesLoaded();
await this.ensureDownloadDirectoriesLoaded();
defaultRootSelectInputs.forEach((select) => {
const category = this.normalizeDownloadCategory(select.dataset.rootCategory || '');
const currentValue = select.value || select.dataset.currentValue || '';
const categoryRoots = Array.isArray(roots?.[category]) ? roots[category] : [];
const options = [
{ value: '', label: 'Auto' },
...categoryRoots.map(path => ({ value: path, label: path }))
];
if (currentValue && !options.some(option => option.value === currentValue)) {
options.push({ value: currentValue, label: `${currentValue} (not currently detected)` });
}
select.innerHTML = options
.map(option => ``)
.join('');
syncDefaultRootContextTarget(select);
});
};
populateDefaultRootSelects();
loadMetadataBuildCapabilities();
setStatus('Saved only on this machine.');
setVisibleSection('mr-options-section-sources');
trackedInputs.forEach((input) => {
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
input.addEventListener(eventName, () => {
setStatus('You have unsaved changes.', 'is-dirty');
});
});
defaultRootSelectInputs.forEach((select) => {
select.addEventListener('change', () => {
syncDefaultRootContextTarget(select);
});
});
this.bindTooltips(this.contentElement);
navButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
if (!targetId) return;
setVisibleSection(targetId);
if (targetId === 'mr-options-section-local-db') {
loadModelListStatus(false);
loadBaseModelsStatus(false);
} else if (targetId === 'mr-options-section-hf') {
loadHfIndexStatus();
}
});
});
if (modelListCheckBtn) {
modelListCheckBtn.addEventListener('click', () => {
loadModelListStatus(true);
});
}
if (modelListUpdateBtn) {
modelListUpdateBtn.addEventListener('click', () => {
updateModelList();
});
}
if (baseModelsCheckBtn) {
baseModelsCheckBtn.addEventListener('click', () => {
loadBaseModelsStatus(true);
});
}
if (baseModelsUpdateBtn) {
baseModelsUpdateBtn.addEventListener('click', () => {
updateBaseModels();
});
}
if (hfIndexRefreshBtn) {
hfIndexRefreshBtn.addEventListener('click', () => {
refreshHfIndex();
});
}
if (exportFrontendLogsBtn) {
exportFrontendLogsBtn.addEventListener('click', () => {
exportFrontendLogs();
});
}
if (exportBackendLogsBtn) {
exportBackendLogsBtn.addEventListener('click', () => {
exportBackendLogs();
});
}
if (clearAllCacheBtn) {
clearAllCacheBtn.addEventListener('click', () => {
clearAllCachesFromOptions();
});
}
if (addDependencyNodeBtn) {
addDependencyNodeBtn.addEventListener('click', () => {
if (typeof window.ModelResolver?.addWorkflowDependencyMarkerNode !== 'function') {
this.showNotification('Could not add Model Resolver opener node.', 'error');
return;
}
window.ModelResolver.addWorkflowDependencyMarkerNode();
});
}
if (metadataSizeAuditBtn) {
metadataSizeAuditBtn.addEventListener('click', () => {
runMetadataSizeAudit();
});
}
if (metadataSizeCollapseToggle) {
metadataSizeCollapseToggle.addEventListener('click', () => {
setMetadataSizeControlsCollapsed(!this.metadataSizeControlsCollapsed);
});
}
if (metadataBuildCollapseToggle) {
metadataBuildCollapseToggle.addEventListener('click', () => {
setMetadataBuildControlsCollapsed(!this.metadataBuildControlsCollapsed);
});
}
if (metadataBuildStartBtn) {
metadataBuildStartBtn.addEventListener('click', () => {
runMetadataBuild();
});
}
if (metadataBuildCancelBtn) {
metadataBuildCancelBtn.addEventListener('click', () => {
cancelMetadataBuild();
});
}
metadataBuildTabButtons.forEach((button) => {
button.addEventListener('click', () => {
setMetadataBuildTab(button.dataset.buildTab || 'progress');
});
});
if (metadataBuildResults) {
metadataBuildResults.addEventListener('click', (event) => {
const filterToggle = event.target?.closest?.('[data-metadata-history-filter-toggle]');
if (filterToggle) {
event.preventDefault();
event.stopPropagation();
setMetadataBuildHistoryFilterMenuOpen(!this.metadataBuildHistoryFilterMenuOpen);
return;
}
const filterOption = event.target?.closest?.('[data-metadata-history-filter-option]');
if (filterOption) {
event.preventDefault();
event.stopPropagation();
const nextFilter = filterOption.dataset.metadataHistoryFilterOption || 'all';
applyMetadataBuildHistoryFilter(nextFilter);
setMetadataBuildHistoryFilterMenuOpen(false);
return;
}
const showMoreButton = event.target?.closest?.('[data-metadata-history-show-more]');
if (showMoreButton) {
event.preventDefault();
event.stopPropagation();
this.metadataBuildHistoryVisibleLimit = Math.max(
metadataBuildHistoryPageSize,
Number(this.metadataBuildHistoryVisibleLimit || metadataBuildHistoryPageSize)
) + metadataBuildHistoryPageSize;
renderMetadataBuildResults(undefined);
}
});
}
this.contentElement.addEventListener('click', (event) => {
if (!this.metadataBuildHistoryFilterMenuOpen) return;
if (event.target?.closest?.('.mr-options-history-action-filter-wrap')) return;
setMetadataBuildHistoryFilterMenuOpen(false);
});
if (metadataBuildResults) {
metadataBuildResults.addEventListener('keydown', (event) => {
if (event.key !== 'Escape' || !this.metadataBuildHistoryFilterMenuOpen) return;
event.preventDefault();
setMetadataBuildHistoryFilterMenuOpen(false);
});
}
if (metadataBuildWorkerInput) {
metadataBuildWorkerInput.addEventListener('change', () => {
metadataBuildWorkerInput.dataset.userEdited = 'true';
const workerCount = normalizeMetadataBuildWorkerCount();
if (metadataBuildWorkersEl && !this.metadataBuildProgressId) {
metadataBuildWorkersEl.textContent = workerCount.toLocaleString();
}
});
}
if (metadataBuildModeSelect) {
metadataBuildModeSelect.addEventListener('change', () => {
metadataBuildModeSelect.dataset.userEdited = 'true';
updateMetadataBuildModeHint();
});
}
if (detectTemplatesBtn) {
detectTemplatesBtn.addEventListener('click', () => {
applyDetectedPathTemplates();
});
}
if (aria2CheckBtn) {
aria2CheckBtn.addEventListener('click', () => {
checkAria2Status({ useUnsavedPath: true });
});
}
if (aria2InstallBtn) {
aria2InstallBtn.addEventListener('click', () => {
installAria2();
});
}
if (aria2StopBtn) {
aria2StopBtn.addEventListener('click', () => {
if (aria2StopBtn.dataset.action === 'start') {
startAria2();
} else {
stopAria2();
}
});
}
if (aria2cPathInput) {
aria2cPathInput.addEventListener('input', () => {
renderAria2Summary({
backend: downloadBackendInput?.value || tokens.download_backend,
configured_path: aria2cPathInput.value
});
setAria2Status('');
});
}
if (downloadBackendInput) {
downloadBackendInput.addEventListener('change', () => {
const backend = this.normalizeDownloadBackend(downloadBackendInput.value);
syncAria2OptionsVisibility();
if (backend === 'aria2') {
checkAria2Status({ useUnsavedPath: true });
}
});
}
syncAria2OptionsVisibility();
if (this.normalizeDownloadBackend(tokens.download_backend) === 'aria2') {
checkAria2Status({ useUnsavedPath: false });
}
const credentialChecks = [
{
input: civitaiSessionInput,
button: civitaiSessionCheckBtn,
statusEl: civitaiSessionCheckStatus,
endpoint: '/model_resolver/civitai/session-token/check',
payloadKey: 'civitai_session_token',
missingText: 'Paste token first'
},
{
input: civitaiInput,
button: civitaiKeyCheckBtn,
statusEl: civitaiKeyCheckStatus,
endpoint: '/model_resolver/civitai/api-key/check',
payloadKey: 'civitai_key',
missingText: 'Paste API key first'
},
{
input: hfInput,
button: hfTokenCheckBtn,
statusEl: hfTokenCheckStatus,
endpoint: '/model_resolver/huggingface/token/check',
payloadKey: 'hf_token',
missingText: 'Paste token first'
},
{
input: braveInput,
button: braveKeyCheckBtn,
statusEl: braveKeyCheckStatus,
endpoint: '/model_resolver/brave/api-key/check',
payloadKey: 'brave_search_api_key',
missingText: 'Paste API key first'
}
];
credentialChecks.forEach((config) => {
if (!config.button) return;
config.button.addEventListener('click', () => {
checkCredential(config);
});
});
loadModelListStatus(false);
loadBaseModelsStatus(false);
loadHfIndexStatus();
if (saveBtn) {
saveBtn.addEventListener('click', async () => {
const civitaiCandidateLimitRaw = parseInt(civitaiLimitInput?.value || `${tokens.civitai_candidate_limit}`, 10);
const civitaiCandidateLimit = Number.isFinite(civitaiCandidateLimitRaw)
? Math.min(20, Math.max(1, civitaiCandidateLimitRaw))
: 5;
// Collect source-enabled flags
const sourceEnabled = {};
if (sourceEnabledInputs.length && !sourceEnabledInputs.some(input => input.checked)) {
const localInput = sourceEnabledInputs.find(input => input.dataset.source === 'local');
if (localInput) localInput.checked = true;
}
sourceEnabledInputs.forEach((input) => {
const key = input.dataset.storageKey;
if (key) {
sourceEnabled[key] = input.checked;
}
});
const downloadPathTemplates = {};
pathTemplateCategories.forEach(category => {
downloadPathTemplates[category.key] = this.normalizeDownloadPathValue(
getTemplateInputValue(category.key)
);
});
const baseModelPathMappings = collectBaseModelPathMappings();
const downloadBackend = this.normalizeDownloadBackend(downloadBackendInput?.value || tokens.download_backend);
const downloadPathMode = this.normalizeDownloadPathMode(downloadPathModeInput?.value || tokens.download_path_mode);
const defaultRootSettings = {};
defaultRootSelectInputs.forEach((select) => {
const settingKey = select.dataset.settingKey;
if (settingKey) {
defaultRootSettings[settingKey] = select.value || '';
}
});
const newSettings = {
civitai_key: civitaiInput?.value || '',
civitai_session_token: civitaiSessionInput?.value || '',
civitai_use_trpc_search: Boolean(civitaiUseTrpcSearchInput?.checked),
civitai_use_api_search: Boolean(civitaiUseApiSearchInput?.checked),
civitai_use_html_fallback: Boolean(civitaiUseHtmlFallbackInput?.checked),
civitai_candidate_limit: civitaiCandidateLimit,
hf_token: hfInput?.value || '',
hf_use_api_search: Boolean(hfUseApiSearchInput?.checked),
hf_use_comfy_org_fallback: Boolean(hfUseComfyOrgFallbackInput?.checked),
hf_use_brave_fallback: Boolean(hfUseBraveFallbackInput?.checked),
auto_fill_base_model: Boolean(autoFillBaseModelInput?.checked),
auto_fill_subfolder: Boolean(autoFillSubfolderInput?.checked),
auto_refresh_comfy_models_after_apply: Boolean(autoRefreshComfyModelsInput?.checked),
auto_open_on_missing: Boolean(autoOpenOnMissingInput?.checked),
workflow_hash_metadata_enabled: Boolean(workflowHashMetadataInput?.checked),
workflow_dependency_marker_enabled: Boolean(workflowDependencyMarkerInput?.checked),
download_backend: downloadBackend,
aria2c_path: aria2cPathInput?.value?.trim() || '',
aria2_auto_stop_daemon: Boolean(aria2AutoStopInput?.checked),
download_sound_enabled: Boolean(downloadSoundInput?.checked),
download_path_mode: downloadPathMode,
download_path_templates: downloadPathTemplates,
base_model_path_mappings: baseModelPathMappings,
...defaultRootSettings,
frontend_logs_enabled: Boolean(frontendLogsEnabledInput?.checked),
backend_logs_enabled: Boolean(backendLogsEnabledInput?.checked),
frontend_log_level: normalizeLogLevel(frontendLogLevelInput?.value || tokens.frontend_log_level),
backend_log_level: normalizeLogLevel(backendLogLevelInput?.value || tokens.backend_log_level),
brave_search_api_key: braveInput?.value || '',
search_source_enabled: sourceEnabled,
};
// 1. Write to safeStorage (fast local cache)
for (const opt of SETTINGS_MAP) {
const val = newSettings[opt.serverKey];
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
safeStorage.setItem(opt.localKey, val ? 'true' : 'false');
} else if (opt.type === 'json') {
safeStorage.setItem(opt.localKey, JSON.stringify(val));
} else {
safeStorage.setItem(opt.localKey, String(val ?? ''));
}
}
defaultRootSelectInputs.forEach((select) => {
const storageKey = select.dataset.storageKey;
if (storageKey) {
safeStorage.setItem(storageKey, select.value || '');
}
});
Object.entries(sourceEnabled).forEach(([key, val]) => {
safeStorage.setItem(key, val ? 'true' : 'false');
});
if (civitaiLimitInput) civitaiLimitInput.value = `${civitaiCandidateLimit}`;
this.applyFrontendLoggingPreference(newSettings.frontend_logs_enabled, newSettings.frontend_log_level);
// 2. Persist to server (survives new browsers / profiles)
try {
setStatus('Saving…', '');
await this.fetchJson('/model_resolver/settings', {
method: 'POST',
body: JSON.stringify(newSettings)
}, 'Save settings');
setStatus('Saved on this machine (all browsers).', 'is-saved');
this.showNotification('Options saved', 'success');
} catch (err) {
console.warn('Model Resolver: could not save settings to server, kept locally only.', err);
setStatus('Saved locally only (server write failed).', 'is-saved');
this.showNotification('Options saved locally only', 'warning');
}
this.downloadRootDirectories = null;
this.downloadSubfolders?.clear();
await this.clearSearchCaches();
});
}
},
normalizeDownloadBackend(value = '') {
const backend = String(value || '').trim().toLowerCase();
return backend === 'aria2' ? 'aria2' : 'python';
},
getStoredTokens() {
const tokens = {};
for (const opt of SETTINGS_MAP) {
const raw = safeStorage.getItem(opt.localKey);
if (opt.type === 'boolean') {
tokens[opt.serverKey] = raw === null ? opt.default : raw !== 'false';
} else if (opt.type === 'json') {
try {
tokens[opt.serverKey] = raw ? JSON.parse(raw) : opt.default;
} catch (_e) {
tokens[opt.serverKey] = opt.default;
}
} else if (opt.type === 'backend') {
tokens[opt.serverKey] = this.normalizeDownloadBackend(raw || opt.default);
} else if (opt.type === 'pathMode') {
tokens[opt.serverKey] = this.normalizeDownloadPathMode(raw || opt.default);
} else if (opt.type === 'frontendLogsEnabled') {
tokens[opt.serverKey] = raw === null ? (frontendLogger.enabled !== false) : raw !== 'false';
} else if (opt.type === 'backendLogsEnabled') {
tokens[opt.serverKey] = raw === null ? true : raw !== 'false';
} else if (opt.type === 'candidateLimit') {
const limit = parseInt(raw || String(opt.default), 10);
tokens[opt.serverKey] = Number.isFinite(limit) ? Math.min(20, Math.max(1, limit)) : opt.default;
} else {
tokens[opt.serverKey] = raw || opt.default;
}
}
tokens.search_source_enabled = this.getSearchSourceEnabledMap();
return {
...tokens,
...this.getDefaultRootSettings()
};
},
applyFrontendLoggingPreference(enabled = true, levelName = 'ERROR') {
frontendLogger.setEnabled(Boolean(enabled));
frontendLogger.setGlobalAndModuleLevel(frontendLogger.normalizeLevel(levelName));
},
/**
* Fetch settings saved on the server and sync them into localStorage.
* Call this once when the dialog initialises so every browser gets the
* same tokens without the user having to re-enter them.
*/
async loadSettingsFromServer() {
try {
const data = await this.fetchJson('/model_resolver/settings', { silent: true }, 'Load settings from server');
if (!data || typeof data !== 'object') {
const tokens = this.getStoredTokens();
this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level);
return;
}
let settingsObj = data;
if (data.schema && Array.isArray(data.schema)) {
SETTINGS_MAP.length = 0;
SETTINGS_MAP.push(...data.schema);
settingsObj = data.settings || {};
}
for (const opt of SETTINGS_MAP) {
const serverValue = settingsObj[opt.serverKey];
if (serverValue === undefined) continue;
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
safeStorage.setItem(opt.localKey, serverValue ? 'true' : 'false');
} else if (opt.type === 'json') {
safeStorage.setItem(opt.localKey, JSON.stringify(serverValue || {}));
} else if (opt.type === 'backend') {
safeStorage.setItem(opt.localKey, this.normalizeDownloadBackend(serverValue));
} else if (opt.type === 'pathMode') {
safeStorage.setItem(opt.localKey, this.normalizeDownloadPathMode(serverValue));
} else {
if (serverValue !== null && serverValue !== '') {
safeStorage.setItem(opt.localKey, String(serverValue));
}
}
}
this.getDefaultRootCategoryDefinitions().forEach((item) => {
if (settingsObj[item.settingKey] !== undefined) {
safeStorage.setItem(item.storageKey, String(settingsObj[item.settingKey] || ''));
}
});
// Source-enabled flags stored as a nested object
if (settingsObj.search_source_enabled && typeof settingsObj.search_source_enabled === 'object') {
Object.entries(settingsObj.search_source_enabled).forEach(([key, val]) => {
if (key) safeStorage.setItem(key, val ? 'true' : 'false');
});
}
const tokens = this.getStoredTokens();
this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level);
} catch (err) {
console.warn('Model Resolver: could not load settings from server, using localStorage only.', err);
const tokens = this.getStoredTokens();
this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level);
}
}
};