diff --git a/src/plugin_system/state_reconciliation.py b/src/plugin_system/state_reconciliation.py index f1e68b8c..e549090a 100644 --- a/src/plugin_system/state_reconciliation.py +++ b/src/plugin_system/state_reconciliation.py @@ -184,8 +184,6 @@ class StateReconciliation: continue if 'enabled' not in plugin_config: continue - if '.standalone-backup-' in plugin_id: - continue state[plugin_id] = { 'enabled': plugin_config.get('enabled', False), 'version': plugin_config.get('version'), @@ -203,6 +201,8 @@ class StateReconciliation: for plugin_dir in self.plugins_dir.iterdir(): if plugin_dir.is_dir(): plugin_id = plugin_dir.name + if '.standalone-backup-' in plugin_id: + continue manifest_path = plugin_dir / "manifest.json" if manifest_path.exists(): import json diff --git a/web_interface/app.py b/web_interface/app.py index 67ab420c..96b8d169 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -652,13 +652,13 @@ def _initialize_health_monitor(): _health_monitor_initialized = True _reconciliation_done = False +_reconciliation_started = False def _run_startup_reconciliation(): - """Run state reconciliation on startup to auto-repair missing plugins.""" + """Run state reconciliation in background to auto-repair missing plugins.""" global _reconciliation_done - if _reconciliation_done: - return - _reconciliation_done = True + from src.logging_config import get_logger + _logger = get_logger('reconciliation') try: from src.plugin_system.state_reconciliation import StateReconciliation @@ -671,20 +671,24 @@ def _run_startup_reconciliation(): ) result = reconciler.reconcile_state() if result.inconsistencies_found: - print(f"[Reconciliation] {result.message}") + _logger.info("[Reconciliation] %s", result.message) if result.inconsistencies_fixed: plugin_manager.discover_plugins() + _reconciliation_done = True except Exception as e: - print(f"[Reconciliation] Error: {e}") + _logger.error("[Reconciliation] Error: %s", e, exc_info=True) # Initialize health monitor and run reconciliation on first request @app.before_request def check_health_monitor(): - """Ensure health monitor and reconciliation run on first request.""" + """Ensure health monitor is initialized; launch reconciliation in background.""" + global _reconciliation_started if not _health_monitor_initialized: _initialize_health_monitor() - if not _reconciliation_done: - _run_startup_reconciliation() + if not _reconciliation_started: + _reconciliation_started = True + import threading + threading.Thread(target=_run_startup_reconciliation, daemon=True).start() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index f232caf0..6a5091cd 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -33,19 +33,25 @@ from src.web_interface.secret_helpers import ( separate_secrets, ) -import re - _SECRET_KEY_PATTERN = re.compile( r'(api_key|api_secret|password|secret|token|auth_key|credential)', re.IGNORECASE, ) -def _conservative_mask_config(config): +def _conservative_mask_config(config, _parent_key=None): """Mask string values whose keys look like secrets (no schema available).""" + if isinstance(config, list): + return [ + _conservative_mask_config(item, _parent_key) if isinstance(item, (dict, list)) + else ('' if isinstance(item, str) and item and _parent_key and _SECRET_KEY_PATTERN.search(_parent_key) else item) + for item in config + ] result = dict(config) for key, value in result.items(): if isinstance(value, dict): result[key] = _conservative_mask_config(value) + elif isinstance(value, list): + result[key] = _conservative_mask_config(value, key) elif isinstance(value, str) and value and _SECRET_KEY_PATTERN.search(key): result[key] = '' return result @@ -2523,7 +2529,12 @@ def get_plugin_config(): # Mask secret fields before returning to prevent exposing API keys schema_mgr = api_v3.schema_manager - schema_for_mask = schema_mgr.load_schema(plugin_id, use_cache=True) if schema_mgr else None + schema_for_mask = None + if schema_mgr: + try: + schema_for_mask = schema_mgr.load_schema(plugin_id, use_cache=True) + except Exception as e: + logger.error("[PluginConfig] Error loading schema for %s: %s", plugin_id, e, exc_info=True) if schema_for_mask and 'properties' in schema_for_mask: plugin_config = mask_secret_fields(plugin_config, schema_for_mask['properties'])