diff --git a/src/plugin_system/plugin_manager.py b/src/plugin_system/plugin_manager.py index dc767eae..406f8d69 100644 --- a/src/plugin_system/plugin_manager.py +++ b/src/plugin_system/plugin_manager.py @@ -119,6 +119,9 @@ class PluginManager: 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(): @@ -136,11 +139,14 @@ class PluginManager: 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 diff --git a/src/plugin_system/state_reconciliation.py b/src/plugin_system/state_reconciliation.py index e549090a..ca381cfd 100644 --- a/src/plugin_system/state_reconciliation.py +++ b/src/plugin_system/state_reconciliation.py @@ -182,10 +182,8 @@ class StateReconciliation: 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 } diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index e6a65d80..fba7dc36 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1785,11 +1785,13 @@ class PluginStoreManager: 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 remote_sha = None @@ -1804,13 +1806,13 @@ class PluginStoreManager: 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): diff --git a/web_interface/app.py b/web_interface/app.py index 4782cbfe..4263dcc8 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -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 @@ -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 @@ -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) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 549d58ce..8a27f43f 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -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 diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index c1a5139e..c0975c10 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -391,566 +391,54 @@ icon.classList.add('fa-chevron-right'); } }; - - // Function to load plugins tab - window.loadPluginsTab = function() { - const content = document.getElementById('plugins-content'); - if (content && !content.hasAttribute('data-loaded')) { - content.setAttribute('data-loaded', 'true'); - console.log('Loading plugins directly via fetch...'); - - fetch('/v3/partials/plugins') - .then(r => r.text()) - .then(html => { - // Parse HTML into a temporary container to extract scripts - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = html; - - // Extract scripts BEFORE inserting into DOM (browser may remove them) - const scripts = Array.from(tempDiv.querySelectorAll('script')); - console.log('Found', scripts.length, 'scripts to execute'); - - // Insert content WITHOUT scripts first - const scriptsToExecute = []; - scripts.forEach(script => { - scriptsToExecute.push({ - content: script.textContent || script.innerHTML, - src: script.src, - type: script.type - }); - script.remove(); // Remove from temp div - }); - - // Now insert the HTML (without scripts) - content.innerHTML = tempDiv.innerHTML; - console.log('Plugins HTML loaded, executing', scriptsToExecute.length, 'scripts...'); - - // Execute scripts manually - ensure they run properly - if (scriptsToExecute.length > 0) { - try { - scriptsToExecute.forEach((scriptData, index) => { - try { - // Skip if script has no content and no src - const scriptContent = scriptData.content ? scriptData.content.trim() : ''; - if (!scriptContent && !scriptData.src) { - return; - } - - // Log script info for debugging - if (scriptContent) { - const preview = scriptContent.substring(0, 100).replace(/\n/g, ' '); - console.log(`[SCRIPT ${index + 1}] Content preview: ${preview}... (${scriptContent.length} chars)`); - - // Check if this script defines our critical functions - if (scriptContent.includes('window.configurePlugin') || scriptContent.includes('window.togglePlugin')) { - console.log(`[SCRIPT ${index + 1}] ⚠️ This script should define configurePlugin/togglePlugin!`); - } - } - - // Only execute if we have valid content - if (scriptContent || scriptData.src) { - // For inline scripts, use appendChild for reliable execution - if (scriptContent && !scriptData.src) { - // For very large scripts (>100KB), try fallback methods first - // as appendChild can sometimes have issues with large scripts - const isLargeScript = scriptContent.length > 100000; - - if (isLargeScript) { - console.log(`[SCRIPT ${index + 1}] Large script detected (${scriptContent.length} chars), trying fallback methods first...`); - - // Try Function constructor first for large scripts - let executed = false; - try { - const func = new Function('window', scriptContent); - func(window); - console.log(`[SCRIPT ${index + 1}] ✓ Executed large script via Function constructor`); - executed = true; - } catch (funcError) { - console.warn(`[SCRIPT ${index + 1}] Function constructor failed:`, funcError.message); - } - - // If Function constructor failed, try indirect eval - if (!executed) { - try { - (0, eval)(scriptContent); - console.log(`[SCRIPT ${index + 1}] ✓ Executed large script via indirect eval`); - executed = true; - } catch (evalError) { - console.warn(`[SCRIPT ${index + 1}] Indirect eval failed:`, evalError.message); - } - } - - // If both fallbacks worked, skip appendChild - if (executed) { - // Verify functions were defined - setTimeout(() => { - console.log(`[SCRIPT ${index + 1}] After fallback execution:`, { - configurePlugin: typeof window.configurePlugin, - togglePlugin: typeof window.togglePlugin - }); - }, 50); - return; // Skip to next script (use return, not continue, in forEach) - } - } - - try { - // Create new script element and append to head/body - // This ensures proper execution context and window attachment - const newScript = document.createElement('script'); - if (scriptData.type) { - newScript.type = scriptData.type; - } - - // Wrap in a promise to wait for execution - const scriptPromise = new Promise((resolve, reject) => { - // Set up error handler - newScript.onerror = (error) => { - reject(error); - }; - - // For inline scripts, execution happens synchronously when appended - // But we'll use a small delay to ensure it completes - try { - // Set textContent (not innerHTML) to avoid execution issues - // Note: We can't wrap in try-catch here as it would interfere with the script - // Instead, we rely on the script's own error handling - newScript.textContent = scriptContent; - - // Append to head for better execution context - const target = document.head || document.body; - if (target) { - // Set up error handler to catch execution errors - newScript.onerror = (error) => { - console.error(`[SCRIPT ${index + 1}] Execution error:`, error); - reject(error); - }; - - // Check before execution - const beforeConfigurePlugin = typeof window.configurePlugin === 'function'; - const beforeTogglePlugin = typeof window.togglePlugin === 'function'; - - // Declare variables in outer scope so setTimeout can access them - let afterConfigurePlugin = beforeConfigurePlugin; - let afterTogglePlugin = beforeTogglePlugin; - - // Append and execute (execution is synchronous for inline scripts) - // Wrap in try-catch to catch any execution errors - try { - target.appendChild(newScript); - - // Check immediately after append (inline scripts execute synchronously) - afterConfigurePlugin = typeof window.configurePlugin === 'function'; - afterTogglePlugin = typeof window.togglePlugin === 'function'; - - console.log(`[SCRIPT ${index + 1}] Immediate check after appendChild:`, { - configurePlugin: { before: beforeConfigurePlugin, after: afterConfigurePlugin }, - togglePlugin: { before: beforeTogglePlugin, after: afterTogglePlugin } - }); - } catch (appendError) { - console.error(`[SCRIPT ${index + 1}] Error during appendChild:`, appendError); - console.error(`[SCRIPT ${index + 1}] Error message:`, appendError.message); - console.error(`[SCRIPT ${index + 1}] Error stack:`, appendError.stack); - - // Try fallback execution methods immediately - console.warn(`[SCRIPT ${index + 1}] Attempting fallback execution methods...`); - let executed = false; - - // Method 1: Function constructor - try { - const func = new Function('window', scriptContent); - func(window); - console.log(`[SCRIPT ${index + 1}] ✓ Executed via Function constructor (fallback)`); - executed = true; - } catch (funcError) { - console.warn(`[SCRIPT ${index + 1}] Function constructor failed:`, funcError.message); - if (funcError.stack) { - console.warn(`[SCRIPT ${index + 1}] Function constructor stack:`, funcError.stack); - } - // Try to find the line number if available - if (funcError.message.includes('line')) { - const lineMatch = funcError.message.match(/line (\d+)/); - if (lineMatch) { - const lineNum = parseInt(lineMatch[1]); - const lines = scriptContent.split('\n'); - const start = Math.max(0, lineNum - 5); - const end = Math.min(lines.length, lineNum + 5); - console.warn(`[SCRIPT ${index + 1}] Context around error (lines ${start}-${end}):`, - lines.slice(start, end).join('\n')); - } - } - } - - // Method 2: Indirect eval - if (!executed) { - try { - (0, eval)(scriptContent); - console.log(`[SCRIPT ${index + 1}] ✓ Executed via indirect eval (fallback)`); - executed = true; - } catch (evalError) { - console.warn(`[SCRIPT ${index + 1}] Indirect eval failed:`, evalError.message); - if (evalError.stack) { - console.warn(`[SCRIPT ${index + 1}] Indirect eval stack:`, evalError.stack); - } - } - } - - // Check if functions are now defined - const fallbackConfigurePlugin = typeof window.configurePlugin === 'function'; - const fallbackTogglePlugin = typeof window.togglePlugin === 'function'; - - console.log(`[SCRIPT ${index + 1}] After fallback attempts:`, { - configurePlugin: fallbackConfigurePlugin, - togglePlugin: fallbackTogglePlugin, - executed: executed - }); - - if (!executed) { - reject(appendError); - } else { - resolve(); - } - } - - // Also check after a small delay to catch any async definitions - setTimeout(() => { - const delayedConfigurePlugin = typeof window.configurePlugin === 'function'; - const delayedTogglePlugin = typeof window.togglePlugin === 'function'; - - // Use the variables from the outer scope - if (delayedConfigurePlugin !== afterConfigurePlugin || delayedTogglePlugin !== afterTogglePlugin) { - console.log(`[SCRIPT ${index + 1}] Functions appeared after delay:`, { - configurePlugin: { immediate: afterConfigurePlugin, delayed: delayedConfigurePlugin }, - togglePlugin: { immediate: afterTogglePlugin, delayed: delayedTogglePlugin } - }); - } - - resolve(); - }, 100); // Small delay to catch any async definitions - } else { - reject(new Error('No target found for script execution')); - } - } catch (appendError) { - reject(appendError); - } - }); - - // Wait for script to execute (with timeout) - Promise.race([ - scriptPromise, - new Promise((_, reject) => setTimeout(() => reject(new Error('Script execution timeout')), 1000)) - ]).catch(error => { - console.warn(`[SCRIPT ${index + 1}] Script execution issue, trying fallback:`, error); - // Fallback: try multiple execution methods - let executed = false; - - // Method 1: Function constructor with window in scope - try { - const func = new Function('window', scriptContent); - func(window); - console.log(`[SCRIPT ${index + 1}] Executed via Function constructor (fallback method 1)`); - executed = true; - } catch (funcError) { - console.warn(`[SCRIPT ${index + 1}] Function constructor failed:`, funcError); - } - - // Method 2: Direct eval in global scope (if method 1 failed) - if (!executed) { - try { - // Use indirect eval to execute in global scope - (0, eval)(scriptContent); - console.log(`[SCRIPT ${index + 1}] Executed via indirect eval (fallback method 2)`); - executed = true; - } catch (evalError) { - console.warn(`[SCRIPT ${index + 1}] Indirect eval failed:`, evalError); - } - } - - // Verify functions after fallback - setTimeout(() => { - console.log(`[SCRIPT ${index + 1}] After fallback execution:`, { - configurePlugin: typeof window.configurePlugin, - togglePlugin: typeof window.togglePlugin, - executed: executed - }); - }, 10); - - if (!executed) { - console.error(`[SCRIPT ${index + 1}] All script execution methods failed`); - console.error(`[SCRIPT ${index + 1}] Script content (first 500 chars):`, scriptContent.substring(0, 500)); - } - }); - } catch (appendError) { - console.error('Failed to execute script:', appendError); - } - } else if (scriptData.src) { - // For external scripts, use appendChild - const newScript = document.createElement('script'); - newScript.src = scriptData.src; - if (scriptData.type) { - newScript.type = scriptData.type; - } - const target = document.head || document.body; - if (target) { - target.appendChild(newScript); - } - console.log('Loaded external script', index + 1, 'of', scriptsToExecute.length); - } - } - } catch (scriptError) { - console.warn('Error executing script', index + 1, ':', scriptError); - } - }); - - // Wait a moment for scripts to execute, then verify functions are available - // Use multiple checks to ensure scripts have time to execute - let checkCount = 0; - const maxChecks = 10; - const checkInterval = setInterval(() => { - checkCount++; - const funcs = { - configurePlugin: typeof window.configurePlugin, - togglePlugin: typeof window.togglePlugin, - updatePlugin: typeof window.updatePlugin, - uninstallPlugin: typeof window.uninstallPlugin, - initializePlugins: typeof window.initializePlugins, - loadInstalledPlugins: typeof window.loadInstalledPlugins, - renderInstalledPlugins: typeof window.renderInstalledPlugins - }; - - if (checkCount === 1 || checkCount === maxChecks) { - console.log('Verifying plugin functions after script execution (check', checkCount, '):', funcs); - } - - // Stop checking once critical functions are available or max checks reached - if ((funcs.configurePlugin === 'function' && funcs.togglePlugin === 'function') || checkCount >= maxChecks) { - clearInterval(checkInterval); - if (funcs.configurePlugin !== 'function' || funcs.togglePlugin !== 'function') { - console.error('Critical plugin functions not available after', checkCount, 'checks'); - } - } - }, 100); - } catch (executionError) { - console.error('Script execution error:', executionError); - } - } else { - console.log('No scripts found in loaded HTML'); - } - - // Wait for scripts to execute, then load plugins - // CRITICAL: Wait for configurePlugin and togglePlugin to be defined before proceeding - let attempts = 0; - const maxAttempts = 20; // Increased to give more time - const checkInterval = setInterval(() => { - attempts++; - - // First, ensure critical functions are available - const criticalFunctionsReady = - window.configurePlugin && typeof window.configurePlugin === 'function' && - window.togglePlugin && typeof window.togglePlugin === 'function'; - - if (!criticalFunctionsReady && attempts < maxAttempts) { - if (attempts % 5 === 0) { // Log every 5th attempt - console.log(`Waiting for critical functions... (attempt ${attempts}/${maxAttempts})`, { - configurePlugin: typeof window.configurePlugin, - togglePlugin: typeof window.togglePlugin - }); - } - return; // Keep waiting - } - - if (!criticalFunctionsReady) { - console.error('Critical functions (configurePlugin, togglePlugin) not available after', maxAttempts, 'attempts'); - clearInterval(checkInterval); - return; - } - - console.log('Critical functions ready, proceeding with plugin initialization...'); - clearInterval(checkInterval); - - // Now try to call initializePlugins first (loads both installed and store) - if (window.initializePlugins && typeof window.initializePlugins === 'function') { - console.log('Found initializePlugins, calling it...'); - window.initializePlugins(); - } else if (window.loadInstalledPlugins && typeof window.loadInstalledPlugins === 'function') { - console.log('Found loadInstalledPlugins, calling it...'); - window.loadInstalledPlugins(); - // Also try to load plugin store - if (window.searchPluginStore && typeof window.searchPluginStore === 'function') { - setTimeout(() => window.searchPluginStore(true), 500); - } - } else if (window.pluginManager && window.pluginManager.loadInstalledPlugins) { - console.log('Found pluginManager.loadInstalledPlugins, calling it...'); - window.pluginManager.loadInstalledPlugins(); - // Also try to load plugin store - setTimeout(() => { - const searchFn = window.searchPluginStore || - (window.pluginManager && window.pluginManager.searchPluginStore); - if (searchFn && typeof searchFn === 'function') { - console.log('Loading plugin store...'); - searchFn(true); - } else { - console.warn('searchPluginStore not available'); - } - }, 500); - } else if (attempts >= maxAttempts) { - console.log('loadInstalledPlugins not found after', maxAttempts, 'attempts, fetching and rendering directly...'); - clearInterval(checkInterval); - - // Load both installed plugins and plugin store - Promise.all([ - // Use batched API requests for better performance - window.PluginAPI && window.PluginAPI.batch ? - window.PluginAPI.batch([ - {endpoint: '/plugins/installed', method: 'GET'}, - {endpoint: '/plugins/store/list?fetch_commit_info=true', method: 'GET'} - ]).then(([installedRes, storeRes]) => { - return [installedRes, storeRes]; - }) : - Promise.all([ - getInstalledPluginsSafe(), - fetch('/api/v3/plugins/store/list?fetch_commit_info=true').then(r => r.json()) - ]) - ]).then(([installedData, storeData]) => { - console.log('Fetched plugins:', installedData); - console.log('Fetched store:', storeData); - - // Render installed plugins - if (installedData.status === 'success') { - const plugins = installedData.data.plugins || []; - const container = document.getElementById('installed-plugins-grid'); - const countEl = document.getElementById('installed-count'); - - // Try renderInstalledPlugins one more time - if (window.renderInstalledPlugins && typeof window.renderInstalledPlugins === 'function') { - console.log('Using renderInstalledPlugins...'); - window.renderInstalledPlugins(plugins); - } else if (container) { - console.log('renderInstalledPlugins not available, rendering full plugin cards manually...'); - // Render full plugin cards with all information - const escapeHtml = function(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - }; - const escapeAttr = function(text) { - return (text || '').replace(/'/g, "\\'").replace(/"/g, '"'); - }; - const escapeJs = function(text) { - return JSON.stringify(text || ''); - }; - const formatCommit = function(commit, branch) { - if (!commit && !branch) return 'Unknown'; - const shortCommit = commit ? String(commit).substring(0, 7) : ''; - const branchText = branch ? String(branch) : ''; - if (branchText && shortCommit) return branchText + ' · ' + shortCommit; - if (branchText) return branchText; - if (shortCommit) return shortCommit; - return 'Unknown'; - }; - const formatDate = function(dateString) { - if (!dateString) return 'Unknown'; - try { - const date = new Date(dateString); - if (isNaN(date.getTime())) return 'Unknown'; - const now = new Date(); - const diffDays = Math.ceil(Math.abs(now - date) / (1000 * 60 * 60 * 24)); - if (diffDays < 1) return 'Today'; - if (diffDays < 2) return 'Yesterday'; - if (diffDays < 7) return diffDays + ' days ago'; - if (diffDays < 30) { - const weeks = Math.floor(diffDays / 7); - return weeks + (weeks === 1 ? ' week' : ' weeks') + ' ago'; - } - return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); - } catch (e) { - return 'Unknown'; - } - }; - container.innerHTML = plugins.map(function(p) { - const name = escapeHtml(p.name || p.id); - const desc = escapeHtml(p.description || 'No description available'); - const author = escapeHtml(p.author || 'Unknown'); - const category = escapeHtml(p.category || 'General'); - const enabled = p.enabled ? 'checked' : ''; - const enabledBool = Boolean(p.enabled); - const escapedId = escapeAttr(p.id); - const verified = p.verified ? 'Verified' : ''; - const tags = (p.tags && p.tags.length > 0) ? '
' + p.tags.map(function(tag) { return '' + escapeHtml(tag) + ''; }).join('') + '
' : ''; - const escapedJsId = escapeJs(p.id); - return '

' + name + '

' + verified + '

' + author + '

' + formatCommit(p.last_commit, p.branch) + '

' + formatDate(p.last_updated) + '

' + category + '

' + desc + '

' + tags + '
'; - }).join(''); - if (countEl) countEl.textContent = plugins.length + ' installed'; - window.installedPlugins = plugins; - console.log('Rendered', plugins.length, 'plugins with full cards'); - } else { - console.error('installed-plugins-grid container not found'); - } - } - - // Render plugin store - if (storeData.status === 'success') { - const storePlugins = storeData.data.plugins || []; - const storeContainer = document.getElementById('plugin-store-grid'); - const storeCountEl = document.getElementById('store-count'); - - if (storeContainer) { - // Try renderPluginStore if available - if (window.renderPluginStore && typeof window.renderPluginStore === 'function') { - console.log('Using renderPluginStore...'); - window.renderPluginStore(storePlugins); - } else { - // Manual rendering fallback - console.log('renderPluginStore not available, rendering manually...'); - const escapeHtml = function(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - }; - const escapeJs = function(text) { - return JSON.stringify(text || ''); - }; - storeContainer.innerHTML = storePlugins.map(function(p) { - const name = escapeHtml(p.name || p.id); - const desc = escapeHtml(p.description || 'No description available'); - const author = escapeHtml(p.author || 'Unknown'); - const category = escapeHtml(p.category || 'General'); - const stars = p.stars || 0; - const verified = p.verified ? 'Verified' : ''; - const escapedJsId = escapeJs(p.id); - return '

' + name + '

' + verified + '

' + author + '

' + category + '

' + (stars > 0 ? '

' + stars + ' stars

' : '') + '

' + desc + '

'; - }).join(''); - } - - if (storeCountEl) { - storeCountEl.innerHTML = storePlugins.length + ' available'; - } - console.log('Rendered', storePlugins.length, 'store plugins'); - } else { - console.error('plugin-store-grid container not found'); - } - } else { - console.error('Failed to load plugin store:', storeData.message); - const storeCountEl = document.getElementById('store-count'); - if (storeCountEl) { - storeCountEl.innerHTML = 'Error loading store'; - } - } - }) - .catch(err => { - console.error('Error fetching plugins/store:', err); - // Still try to render installed plugins if store fails - }); - } - }, 100); // Reduced from 200ms to 100ms for faster retries - }) - .catch(err => console.error('Error loading plugins:', err)); - } - }; })(); + +