diff --git a/README.md b/README.md index f53a6f5..3ed0391 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Python 3.10+ JavaScript - SponsorGitHub + SponsorGitHub

🔹 Quick Start diff --git a/__init__.py b/__init__.py index 4ed6483..b64968e 100644 --- a/__init__.py +++ b/__init__.py @@ -15,6 +15,8 @@ from .core.log_system.log_funcs import ( log_exception, log_warn, ) +from .core.log_system.config import LOG_LEVEL as BACKEND_DEFAULT_LOG_LEVEL +from .core.log_system.logger import LogLevel, logger as backend_log_controller # Web directory for JavaScript interface WEB_DIRECTORY = "./web" @@ -2091,13 +2093,42 @@ class ModelResolverExtension: self.logger.warning(f"Model Resolver: could not read settings file: {exc}") return {} - def _save_settings(payload: dict) -> None: + def _bool_setting(value, default: bool = True) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + return bool(value) + + def _log_level_setting(value, default: str = BACKEND_DEFAULT_LOG_LEVEL) -> LogLevel: + normalized = str(value or default or "INFO").strip().upper() + if hasattr(LogLevel, normalized): + return getattr(LogLevel, normalized) + fallback = str(default or "INFO").strip().upper() + return getattr(LogLevel, fallback, LogLevel.INFO) + + def _apply_backend_logging_settings(settings: dict) -> None: + enabled = _bool_setting(settings.get("backend_logs_enabled"), True) + level = _log_level_setting(settings.get("backend_log_level")) + backend_log_controller.set_enabled(enabled) + backend_log_controller.set_global_level(level) + + def _save_settings(payload: dict) -> dict: # Merge with existing settings so we never lose keys not sent # by the current request. current = _load_settings() current.update({k: v for k, v in payload.items() if k}) with open(_SETTINGS_FILE, "w", encoding="utf-8") as f: _json.dump(current, f, indent=2, ensure_ascii=False) + return current + + _apply_backend_logging_settings(_load_settings()) @routes.get("/model_resolver/settings") async def get_settings_route(request): @@ -2116,7 +2147,8 @@ class ModelResolverExtension: payload = await request.json() if not isinstance(payload, dict): return web.json_response({"error": "Expected JSON object"}, status=400) - await asyncio.to_thread(_save_settings, payload) + settings = await asyncio.to_thread(_save_settings, payload) + _apply_backend_logging_settings(settings) return web.json_response({"success": True}) except Exception as e: self.logger.error(f"Model Resolver settings POST error: {e}", exc_info=True) diff --git a/core/log_system/config.py b/core/log_system/config.py index 9829c15..1827149 100644 --- a/core/log_system/config.py +++ b/core/log_system/config.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 1.5.2 +@version: 1.5.6 @description: Logging Configuration """ # Log level for development. diff --git a/core/log_system/log_funcs.py b/core/log_system/log_funcs.py index 89d00b5..e80a495 100644 --- a/core/log_system/log_funcs.py +++ b/core/log_system/log_funcs.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 1.5.2 +@version: 1.5.6 @description: Logging Initializator """ # ruff: noqa: T201 diff --git a/core/log_system/logger.py b/core/log_system/logger.py index dd28363..c0a5d49 100644 --- a/core/log_system/logger.py +++ b/core/log_system/logger.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 1.5.2 +@version: 1.5.6 @description: Logging Setup - Central logging system Features: diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 5314118..bef9552 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -4670,7 +4670,8 @@ min-width: 0; padding-right: 8px; } -.mr-options-input { +.mr-options-input, +#model-resolver-modal select.mr-options-input { width: 100%; min-width: 0; min-height: 42px; @@ -4683,7 +4684,8 @@ outline: none; transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease; } -.mr-options-input:focus { +.mr-options-input:focus, +#model-resolver-modal select.mr-options-input:focus { background: #0f1013; border-color: rgba(78,161,255,0.45); box-shadow: none; diff --git a/web/log_system/config.js b/web/log_system/config.js index 7997e06..6373fb8 100644 --- a/web/log_system/config.js +++ b/web/log_system/config.js @@ -1,7 +1,7 @@ /** author: Azornes title: AzLogs -version: 1.5.5 +version: 1.5.6 description: Logging Configuration */ // Log level for development. diff --git a/web/log_system/log_funcs.js b/web/log_system/log_funcs.js index d219ae5..e676f74 100644 --- a/web/log_system/log_funcs.js +++ b/web/log_system/log_funcs.js @@ -1,7 +1,7 @@ /** author: Azornes title: AzLogs -version: 1.5.5 +version: 1.5.6 description: Logging Initializator Features: @@ -17,7 +17,10 @@ import { LOG_LEVEL } from './config.js'; * @returns {Logger} Object with logging methods */ export function createModuleLogger(moduleName) { - logger.setModuleLevel(moduleName, LogLevel[LOG_LEVEL]); + logger.config.moduleSettings = logger.config.moduleSettings || {}; + if (logger.config.moduleSettings[moduleName] === undefined) { + logger.setModuleLevel(moduleName, logger.normalizeLevel(logger.config.globalLevel, LogLevel[LOG_LEVEL])); + } return { debug: (...args) => logger.debug(moduleName, ...args), info: (...args) => logger.info(moduleName, ...args), diff --git a/web/log_system/logger.js b/web/log_system/logger.js index 0726d8e..1c0d78f 100644 --- a/web/log_system/logger.js +++ b/web/log_system/logger.js @@ -13,7 +13,7 @@ logger - Central logging system - Ability to export logs */ -import { DEFAULT_LOGGER_NAME, LOG_MODULE_NAME, USE_COLORS } from './config.js'; +import { DEFAULT_LOGGER_NAME, LOG_LEVEL, LOG_MODULE_NAME, USE_COLORS } from './config.js'; function padStart(str, targetLength, padString) { targetLength = targetLength >> 0; @@ -53,7 +53,8 @@ export const LogLevel = { NONE: 4 }; const DEFAULT_CONFIG = { - globalLevel: LogLevel.INFO, + enabled: true, + globalLevel: LogLevel[LOG_LEVEL] ?? LogLevel.INFO, moduleSettings: {}, useColors: USE_COLORS, saveToStorage: false, @@ -90,6 +91,7 @@ class Logger { this.logs = []; this.enabled = true; this.loadConfig(); + this.enabled = this.config.enabled !== false; this.loadLogs(); } /** @@ -98,6 +100,17 @@ class Logger { */ configure(config) { this.config = { ...this.config, ...config }; + if (Object.prototype.hasOwnProperty.call(config, 'enabled')) { + this.enabled = config.enabled !== false; + } + this.config.globalLevel = this.normalizeLevel(this.config.globalLevel, DEFAULT_CONFIG.globalLevel); + if (!this.config.moduleSettings || typeof this.config.moduleSettings !== 'object') { + this.config.moduleSettings = {}; + } else { + Object.entries(this.config.moduleSettings).forEach(([module, level]) => { + this.config.moduleSettings[module] = this.normalizeLevel(level, this.config.globalLevel); + }); + } this.saveConfig(); return this; } @@ -106,7 +119,9 @@ class Logger { * @param {boolean} enabled - Whether the logger should be enabled */ setEnabled(enabled) { - this.enabled = enabled; + this.enabled = Boolean(enabled); + this.config.enabled = this.enabled; + this.saveConfig(); return this; } /** @@ -114,7 +129,32 @@ class Logger { * @param {LogLevels} level - Logging level */ setGlobalLevel(level) { - this.config.globalLevel = level; + this.config.globalLevel = this.normalizeLevel(level, DEFAULT_CONFIG.globalLevel); + this.saveConfig(); + return this; + } + normalizeLevel(level, fallback = this.config.globalLevel) { + if (typeof level === 'number' && Object.values(LogLevel).includes(level)) { + return level; + } + if (typeof level === 'string') { + const normalized = level.trim().toUpperCase(); + if (Object.prototype.hasOwnProperty.call(LogLevel, normalized)) { + return LogLevel[normalized]; + } + } + return fallback; + } + getLevelName(level = this.config.globalLevel) { + const normalizedLevel = this.normalizeLevel(level, this.config.globalLevel); + return Object.entries(LogLevel).find(([, value]) => value === normalizedLevel)?.[0] || 'INFO'; + } + setGlobalAndModuleLevel(level) { + const normalizedLevel = this.normalizeLevel(level, this.config.globalLevel); + this.config.globalLevel = normalizedLevel; + Object.keys(this.config.moduleSettings || {}).forEach((module) => { + this.config.moduleSettings[module] = normalizedLevel; + }); this.saveConfig(); return this; } @@ -124,7 +164,7 @@ class Logger { * @param {LogLevels} level - Logging level */ setModuleLevel(module, level) { - this.config.moduleSettings[module] = level; + this.config.moduleSettings[module] = this.normalizeLevel(level, this.config.globalLevel); this.saveConfig(); return this; } @@ -597,6 +637,7 @@ class Logger { const storedConfig = localStorage.getItem(LOGGER_CONFIG_KEY); if (storedConfig) { this.config = { ...this.config, ...JSON.parse(storedConfig) }; + this.configure(this.config); } } catch (e) { diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 7332bba..3f16ba1 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -1,13 +1,17 @@ import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; +import { createModuleLogger } from "../../log_system/log_funcs.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; + +const log = createModuleLogger('resolve_download_methods'); + export const resolveDownloadMethods = { /** * Resolve a model - resolves ALL nodes that reference this model */ async resolveModel(missing, resolvedModel) { - console.log('resolveModel called:', missing?.original_path, '->', resolvedModel?.filename); + log.debug('resolveModel called:', missing?.original_path, '->', resolvedModel?.filename); if (!resolvedModel) { this.showNotification('No resolved model selected', 'error'); @@ -23,7 +27,7 @@ export const resolveDownloadMethods = { // Resolve ALL nodes that need this model (all_node_refs contains deduplicated refs) const nodeRefs = missing.all_node_refs || [missing]; - console.log('nodeRefs count:', nodeRefs?.length, 'is_lora_v2:', nodeRefs?.[0]?.is_lora_v2); + log.debug('nodeRefs count:', nodeRefs?.length, 'is_lora_v2:', nodeRefs?.[0]?.is_lora_v2); const resolutions = nodeRefs.map(ref => ({ node_id: ref.node_id, @@ -51,7 +55,7 @@ export const resolveDownloadMethods = { } const data = await response.json(); - console.log('Resolve response: success=', data.success, ' missing count:', data.workflow?.nodes?.length); + log.debug('Resolve response: success=', data.success, ' missing count:', data.workflow?.nodes?.length); if (data.success) { // Update workflow in ComfyUI @@ -1366,7 +1370,7 @@ export const resolveDownloadMethods = { }; try { - console.log('Model Resolver: Search request:', JSON.stringify(searchData)); + log.debug('Model Resolver: Search request:', JSON.stringify(searchData)); const response = await api.fetchApi('/model_resolver/search', { method: 'POST', @@ -1379,7 +1383,7 @@ export const resolveDownloadMethods = { } const data = await response.json(); - console.log('Model Resolver: Search response:', JSON.stringify(data)); + log.debug('Model Resolver: Search response:', JSON.stringify(data)); if (!this.isBackgroundSearchRunActive(workflowKey, missingSearchKey, searchRunId)) { return { source, stale: true }; @@ -1511,9 +1515,9 @@ export const resolveDownloadMethods = { * Resolve URN asynchronously - fetch CivitAI info and update UI */ async resolveUrnAsync(modelId, versionId, loadingElementId, modelUrl) { - console.log('resolveUrnAsync called:', modelId, versionId); + log.debug('resolveUrnAsync called:', modelId, versionId); if (!modelId || !versionId) { - console.log('resolveUrnAsync: missing modelId or versionId'); + log.debug('resolveUrnAsync: missing modelId or versionId'); return; } @@ -1537,7 +1541,7 @@ export const resolveDownloadMethods = { version_id: versionId, civitai_candidate_limit: tokens.civitai_candidate_limit }; - console.log('resolveUrnAsync payload:', JSON.stringify(payload)); + log.debug('resolveUrnAsync payload:', JSON.stringify(payload)); const response = await api.fetchApi('/model_resolver/search', { method: 'POST', @@ -1545,7 +1549,7 @@ export const resolveDownloadMethods = { body: JSON.stringify(payload) }); - console.log('resolveUrnAsync response status:', response.status); + log.debug('resolveUrnAsync response status:', response.status); if (!response.ok) { throw new Error(`URN resolve failed: ${response.status}`); } diff --git a/web/resolver/model_resolver.js b/web/resolver/model_resolver.js index fd121a8..c354fd2 100644 --- a/web/resolver/model_resolver.js +++ b/web/resolver/model_resolver.js @@ -1,9 +1,24 @@ import { app } from "../../../../scripts/app.js"; import { api } from "../../../../scripts/api.js"; import { $el } from "../../../../scripts/ui.js"; +import { createModuleLogger } from "../log_system/log_funcs.js"; +import { logger as frontendLogger } from "../log_system/logger.js"; import { loadStylesWhenNeeded } from "../utils/css_loader.js"; import { ResolverManagerDialog } from "./resolver_dialog.js"; +const log = createModuleLogger('model_resolver'); + +function applyStoredFrontendLoggingPreference() { + const stored = localStorage.getItem('ModelResolver.frontendLogsEnabled'); + if (stored !== null) { + frontendLogger.setEnabled(stored !== 'false'); + } + const storedLevel = localStorage.getItem('ModelResolver.frontendLogLevel'); + if (storedLevel) { + frontendLogger.setGlobalAndModuleLevel(frontendLogger.normalizeLevel(storedLevel)); + } +} + // Main extension class export class ModelResolver { constructor() { @@ -19,6 +34,7 @@ export class ModelResolver { } setup = async () => { + applyStoredFrontendLoggingPreference(); loadStylesWhenNeeded(); // Remove any existing button @@ -176,7 +192,7 @@ export class ModelResolver { app.menu?.settingsGroup.element.before(this.buttonGroup.element); } catch (e) { // Fallback for older ComfyUI versions without the new button system - console.log('Model Resolver: New button system not available, using floating button fallback.'); + log.debug('Model Resolver: New button system not available, using floating button fallback.'); this.createFloatingButton(); } } @@ -188,7 +204,7 @@ export class ModelResolver { // Watch for ComfyUI's Missing Models popup and inject our button this.setupMissingModelsPopupObserver(); - console.log('Model Resolver: Missing models popup button injection enabled'); + log.debug('Model Resolver: Missing models popup button injection enabled'); } setupActiveWorkflowChangeListeners() { @@ -405,7 +421,7 @@ export class ModelResolver { } } - console.log('Model Resolver: Injected buttons into Missing Models popup'); + log.debug('Model Resolver: Injected buttons into Missing Models popup'); }; // Small delay to ensure popup is fully rendered @@ -450,7 +466,7 @@ export class ModelResolver { * Mark resolved model items in the popup as linked (green) and hide download buttons */ removeResolvedFromPopup(dialog, resolvedFilenames) { - console.log('Model Resolver: Looking for resolved filenames:', resolvedFilenames); + log.debug('Model Resolver: Looking for resolved filenames:', resolvedFilenames); // Strategy: For each filename, find text nodes containing it, // then find the nearest Download button and mark that row @@ -477,7 +493,7 @@ export class ModelResolver { !btn.id?.includes('model-resolver')); if (downloadBtn) { - console.log('Model Resolver: Found entry for', filename); + log.debug('Model Resolver: Found entry for', filename); this.markEntryAsResolved(parent, downloadBtn); break; } @@ -501,7 +517,7 @@ export class ModelResolver { if (container.dataset.resolved === 'true') return; container.dataset.resolved = 'true'; - console.log('Model Resolver: Marking entry as resolved', container); + log.debug('Model Resolver: Marking entry as resolved', container); // Add green background/styling to the container container.classList.add('mr-resolved-entry'); @@ -574,11 +590,11 @@ export class ModelResolver { widget.callback(resolvedPath, app.graph, node, null, null); } - console.log(`Model Resolver: Updated node ${nodeId} widget ${widgetIndex} to ${resolvedPath}`); + log.debug(`Model Resolver: Updated node ${nodeId} widget ${widgetIndex} to ${resolvedPath}`); } else if (node.widgets_values) { // Fallback: update widgets_values array directly node.widgets_values[widgetIndex] = resolvedPath; - console.log(`Model Resolver: Updated node ${nodeId} widgets_values[${widgetIndex}] to ${resolvedPath}`); + log.debug(`Model Resolver: Updated node ${nodeId} widgets_values[${widgetIndex}] to ${resolvedPath}`); } // Mark node as dirty to trigger redraw @@ -658,7 +674,7 @@ export class ModelResolver { // Auto-open dialog if there are missing models if (data.total_missing > 0) { - console.log(`Model Resolver: Found ${data.total_missing} missing model(s), opening dialog...`); + log.debug(`Model Resolver: Found ${data.total_missing} missing model(s), opening dialog...`); this.openResolverManager(); } diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index 6fd458e..b7c3f64 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -1,6 +1,8 @@ import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; +import { LOG_LEVEL as DEFAULT_FRONTEND_LOG_LEVEL } from "../../log_system/config.js"; +import { logger as frontendLogger } from "../../log_system/logger.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; export const downloadTargetMethods = { /** @@ -860,6 +862,10 @@ export const downloadTargetMethods = { ? Math.min(20, Math.max(1, civitaiCandidateLimitRaw)) : 5; const search_source_enabled = this.getSearchSourceEnabledMap(); + const storedFrontendLogsEnabled = localStorage.getItem('ModelResolver.frontendLogsEnabled'); + const storedBackendLogsEnabled = localStorage.getItem('ModelResolver.backendLogsEnabled'); + const storedFrontendLogLevel = localStorage.getItem('ModelResolver.frontendLogLevel'); + const storedBackendLogLevel = localStorage.getItem('ModelResolver.backendLogLevel'); return { civitai_key: localStorage.getItem('ModelResolver.civitaiApiKey') || '', @@ -873,11 +879,24 @@ export const downloadTargetMethods = { hf_use_brave_fallback: localStorage.getItem('ModelResolver.hfUseBraveFallback') !== 'false', auto_fill_base_model: localStorage.getItem('ModelResolver.autoFillBaseModel') !== 'false', auto_fill_subfolder: localStorage.getItem('ModelResolver.autoFillSubfolder') !== 'false', + frontend_logs_enabled: storedFrontendLogsEnabled === null + ? frontendLogger.enabled !== false + : storedFrontendLogsEnabled !== 'false', + backend_logs_enabled: storedBackendLogsEnabled === null + ? true + : storedBackendLogsEnabled !== 'false', + frontend_log_level: storedFrontendLogLevel || DEFAULT_FRONTEND_LOG_LEVEL, + backend_log_level: storedBackendLogLevel || 'DEBUG', civitai_candidate_limit, search_source_enabled }; }, + applyFrontendLoggingPreference(enabled = true, levelName = 'DEBUG') { + 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 @@ -886,9 +905,17 @@ export const downloadTargetMethods = { async loadSettingsFromServer() { try { const resp = await api.fetchApi('/model_resolver/settings'); - if (!resp.ok) return; + if (!resp.ok) { + const tokens = this.getStoredTokens(); + this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level); + return; + } const data = await resp.json(); - if (!data || typeof data !== 'object') return; + if (!data || typeof data !== 'object') { + const tokens = this.getStoredTokens(); + this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level); + return; + } // Helper: write to localStorage only when the value from server is // non-empty, so we don't overwrite a key the user already has locally. @@ -919,6 +946,14 @@ export const downloadTargetMethods = { localStorage.setItem('ModelResolver.autoFillSubfolder', data.auto_fill_subfolder ? 'true' : 'false'); if (data.civitai_candidate_limit !== undefined) localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${data.civitai_candidate_limit}`); + if (data.frontend_logs_enabled !== undefined) + localStorage.setItem('ModelResolver.frontendLogsEnabled', data.frontend_logs_enabled ? 'true' : 'false'); + if (data.backend_logs_enabled !== undefined) + localStorage.setItem('ModelResolver.backendLogsEnabled', data.backend_logs_enabled ? 'true' : 'false'); + if (data.frontend_log_level !== undefined) + localStorage.setItem('ModelResolver.frontendLogLevel', String(data.frontend_log_level || 'DEBUG').toUpperCase()); + if (data.backend_log_level !== undefined) + localStorage.setItem('ModelResolver.backendLogLevel', String(data.backend_log_level || 'DEBUG').toUpperCase()); // Source-enabled flags stored as a nested object if (data.search_source_enabled && typeof data.search_source_enabled === 'object') { @@ -926,8 +961,12 @@ export const downloadTargetMethods = { if (key) localStorage.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); } }, diff --git a/web/resolver/shell/workflow_state_methods.js b/web/resolver/shell/workflow_state_methods.js index b260976..0cb9133 100644 --- a/web/resolver/shell/workflow_state_methods.js +++ b/web/resolver/shell/workflow_state_methods.js @@ -1,7 +1,11 @@ import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; +import { createModuleLogger } from "../../log_system/log_funcs.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; + +const log = createModuleLogger('workflow_state_methods'); + export const workflowStateMethods = { getWorkflowSignature(workflow) { if (!workflow) return null; @@ -564,7 +568,7 @@ export const workflowStateMethods = { return; } - console.log('Model Resolver: active workflow changed, refreshing current tab', { + log.debug('Model Resolver: active workflow changed, refreshing current tab', { reason, route: currentRoute, tab: this.activeTab diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index f041638..f865fc0 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -31,6 +31,17 @@ export const optionsMethods = { `; }) .join(''); + const logLevelValues = ['DEBUG', 'INFO', 'WARN', 'ERROR', '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 = `

@@ -374,6 +385,48 @@ export const optionsMethods = {

Maintenance

+
+
+ +
+
+
+ Frontend log level ? +
+ +
+
+ +
+
+
+ Backend log level ? +
+ +
+
+
+
@@ -412,6 +465,10 @@ export const optionsMethods = { 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 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 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'); @@ -453,6 +510,10 @@ export const optionsMethods = { hfUseBraveFallbackInput, autoFillBaseModelInput, autoFillSubfolderInput, + frontendLogsEnabledInput, + backendLogsEnabledInput, + frontendLogLevelInput, + backendLogLevelInput, ...sourceEnabledInputs, ].filter(Boolean); @@ -836,7 +897,7 @@ export const optionsMethods = { setVisibleSection('mr-options-section-sources'); trackedInputs.forEach((input) => { - const eventName = input.type === 'checkbox' ? 'change' : 'input'; + const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input'; input.addEventListener(eventName, () => { setStatus('You have unsaved changes.', 'is-dirty'); }); @@ -982,6 +1043,10 @@ export const optionsMethods = { hf_use_brave_fallback: Boolean(hfUseBraveFallbackInput?.checked), auto_fill_base_model: Boolean(autoFillBaseModelInput?.checked), auto_fill_subfolder: Boolean(autoFillSubfolderInput?.checked), + 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, }; @@ -998,11 +1063,16 @@ export const optionsMethods = { localStorage.setItem('ModelResolver.hfUseBraveFallback', newSettings.hf_use_brave_fallback ? 'true' : 'false'); localStorage.setItem('ModelResolver.autoFillBaseModel', newSettings.auto_fill_base_model ? 'true' : 'false'); localStorage.setItem('ModelResolver.autoFillSubfolder', newSettings.auto_fill_subfolder ? 'true' : 'false'); + localStorage.setItem('ModelResolver.frontendLogsEnabled', newSettings.frontend_logs_enabled ? 'true' : 'false'); + localStorage.setItem('ModelResolver.backendLogsEnabled', newSettings.backend_logs_enabled ? 'true' : 'false'); + localStorage.setItem('ModelResolver.frontendLogLevel', newSettings.frontend_log_level); + localStorage.setItem('ModelResolver.backendLogLevel', newSettings.backend_log_level); localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`); Object.entries(sourceEnabled).forEach(([key, val]) => { localStorage.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 {