Skip to content
Merged
8 changes: 7 additions & 1 deletion src/plugin_system/plugin_manager.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,6 +119,9 @@ def _scan_directory_for_plugins(self, directory: Path) -> List[str]:
for item in directory.iterdir():
if not item.is_dir():
continue
# Skip backup directories so they don't overwrite live entries
if '.standalone-backup-' in item.name:
continue

manifest_path = item / "manifest.json"
if manifest_path.exists():
Expand All@@ -136,11 +139,14 @@ def _scan_directory_for_plugins(self, directory: Path) -> List[str]:
except (OSError, PermissionError) as e:
self.logger.error("Error scanning directory %s: %s", directory, e, exc_info=True)

# Update shared state under lock
# Replace shared state under lock so uninstalled plugins don't linger
with self._discovery_lock:
self.plugin_manifests.clear()
self.plugin_manifests.update(new_manifests)
if not hasattr(self, 'plugin_directories'):
self.plugin_directories = {}
else:
self.plugin_directories.clear()
self.plugin_directories.update(new_directories)

return plugin_ids
Expand Down
4 changes: 1 addition & 3 deletions src/plugin_system/state_reconciliation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,10 +182,8 @@ def _get_config_state(self) -> Dict[str, Dict[str, Any]]:
continue
if plugin_id in self._SYSTEM_CONFIG_KEYS:
continue
if 'enabled' not in plugin_config:
continue
state[plugin_id] = {
'enabled': plugin_config.get('enabled', False),
'enabled': plugin_config.get('enabled', True),
'version': plugin_config.get('version'),
'exists_in_config': True
}
Expand Down
10 changes: 6 additions & 4 deletions src/plugin_system/store_manager.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1785,11 +1785,13 @@ def update_plugin(self, plugin_id: str) -> bool:
self.fetch_registry(force_refresh=True)
plugin_info_remote = self.get_plugin_info(plugin_id, fetch_latest_from_github=True, force_refresh=True)
# Try without 'ledmatrix-' prefix (monorepo migration)
resolved_id = plugin_id
if not plugin_info_remote and plugin_id.startswith('ledmatrix-'):
alt_id = plugin_id[len('ledmatrix-'):]
plugin_info_remote = self.get_plugin_info(alt_id, fetch_latest_from_github=True, force_refresh=True)
if plugin_info_remote:
self.logger.info(f"Plugin {plugin_id} found in registry as {alt_id}")
resolved_id = alt_id
self.logger.info(f"Plugin {plugin_id} found in registry as {resolved_id}")
remote_branch = None
Comment thread
ChuckBuilds marked this conversation as resolved.
remote_sha = None

Expand All@@ -1804,13 +1806,13 @@ def update_plugin(self, plugin_id: str) -> bool:
local_remote = git_info.get('remote_url', '')
if local_remote and registry_repo and self._normalize_repo_url(local_remote) != self._normalize_repo_url(registry_repo):
self.logger.info(
f"Plugin {plugin_id} git remote ({local_remote}) differs from registry ({registry_repo}). "
f"Plugin {resolved_id} git remote ({local_remote}) differs from registry ({registry_repo}). "
f"Reinstalling from registry to migrate to new source."
)
if not self._safe_remove_directory(plugin_path):
self.logger.error(f"Failed to remove old plugin directory for {plugin_id}")
self.logger.error(f"Failed to remove old plugin directory for {resolved_id}")
return False
return self.install_plugin(plugin_id)
return self.install_plugin(resolved_id)

# Check if already up to date
if remote_sha and local_sha and remote_sha.startswith(local_sha):
Expand Down
18 changes: 11 additions & 7 deletions web_interface/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -653,8 +653,10 @@ def _initialize_health_monitor():

_reconciliation_done = False
_reconciliation_started = False
import threading as _threading
_reconciliation_lock = _threading.Lock()

def _run_startup_reconciliation():
def _run_startup_reconciliation() -> None:
"""Run state reconciliation in background to auto-repair missing plugins."""
global _reconciliation_done, _reconciliation_started
from src.logging_config import get_logger
Expand All@@ -678,10 +680,12 @@ def _run_startup_reconciliation():
_reconciliation_done = True
else:
_logger.warning("[Reconciliation] Finished with unresolved issues, will retry")
_reconciliation_started = False
with _reconciliation_lock:
_reconciliation_started = False
except Exception as e:
_logger.error("[Reconciliation] Error: %s", e, exc_info=True)
_reconciliation_started = False
with _reconciliation_lock:
_reconciliation_started = False

# Initialize health monitor and run reconciliation on first request
@app.before_request
Expand All@@ -690,10 +694,10 @@ def check_health_monitor():
global _reconciliation_started
if not _health_monitor_initialized:
_initialize_health_monitor()
if not _reconciliation_started:
_reconciliation_started = True
import threading
threading.Thread(target=_run_startup_reconciliation, daemon=True).start()
with _reconciliation_lock:
if not _reconciliation_started:
_reconciliation_started = True
_threading.Thread(target=_run_startup_reconciliation, daemon=True).start()

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
29 changes: 14 additions & 15 deletions web_interface/static/v3/plugins_manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1157,21 +1157,20 @@ function initializePluginPageWhenReady() {

// Strategy 3: HTMX afterSwap event (for HTMX-loaded content)
// This is the primary way plugins content is loaded
if (typeof htmx !== 'undefined') {
document.body.addEventListener('htmx:afterSwap', function(event) {
const target = event.detail.target;
// Check if plugins content was swapped in
if (target.id === 'plugins-content' ||
target.querySelector('#installed-plugins-grid') ||
document.getElementById('installed-plugins-grid')) {
console.log('HTMX swap detected for plugins, initializing...');
// Reset initialization flag to allow re-initialization after HTMX swap
window.pluginManager.initialized = false;
window.pluginManager.initializing = false;
initTimer = setTimeout(attemptInit, 100);
}
}, { once: false }); // Allow multiple swaps
}
// Register unconditionally — HTMX may load after this script (loaded dynamically from CDN)
// CustomEvent listeners work even before HTMX is available
document.body.addEventListener('htmx:afterSwap', function(event) {
const target = event.detail.target;
// Check if plugins content was swapped in (only match direct plugins content targets)
if (target.id === 'plugins-content' ||
target.querySelector('#installed-plugins-grid')) {
console.log('HTMX swap detected for plugins, initializing...');
// Reset initialization flag to allow re-initialization after HTMX swap
window.pluginManager.initialized = false;
window.pluginManager.initializing = false;
initTimer = setTimeout(attemptInit, 100);
}
}, { once: false }); // Allow multiple swaps
})();

// Initialization guard to prevent multiple initializations
Expand Down
Loading