mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
fix: address code review findings for plugin auto-repair
- Move backup-folder filter from _get_config_state to _get_disk_state where the artifact actually lives - Run startup reconciliation in a background thread so requests aren't blocked by plugin reinstallation - Set _reconciliation_done only after success so failures allow retries - Replace print() with proper logger in reconciliation - Wrap load_schema in try/except so exceptions fall through to conservative masking instead of 500 - Handle list values in _conservative_mask_config for nested secrets - Remove duplicate import re Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user