diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index b946100a..15760880 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -86,7 +86,7 @@ window.requestOnDemandStop = function({ stopService = false } = {}) { // Define updatePlugin early as a stub to ensure it's always available window.updatePlugin = window.updatePlugin || function(pluginId) { if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS STUB] updatePlugin called for', pluginId); - + // Validate pluginId if (!pluginId || typeof pluginId !== 'string') { console.error('Invalid pluginId:', pluginId); @@ -95,22 +95,22 @@ window.updatePlugin = window.updatePlugin || function(pluginId) { } return Promise.reject(new Error('Invalid plugin ID')); } - + // Show immediate feedback if (typeof showNotification === 'function') { showNotification(`Updating ${pluginId}...`, 'info'); } - + // Prepare request body const requestBody = { plugin_id: pluginId }; const requestBodyJson = JSON.stringify(requestBody); - + console.log('[UPDATE] Sending request:', { url: '/api/v3/plugins/update', body: requestBodyJson }); - + // Make the API call directly return fetch('/api/v3/plugins/update', { method: 'POST', - headers: { + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, @@ -128,13 +128,13 @@ window.updatePlugin = window.updatePlugin || function(pluginId) { } catch (e) { errorData = { message: `Server error: ${response.status} ${response.statusText}` }; } - + if (typeof showNotification === 'function') { showNotification(errorData.message || `Update failed: ${response.status}`, 'error'); } throw new Error(errorData.message || `Update failed: ${response.status}`); } - + // Parse successful response return response.json(); }) @@ -162,15 +162,15 @@ window.updatePlugin = window.updatePlugin || function(pluginId) { // Define uninstallPlugin early as a stub window.uninstallPlugin = window.uninstallPlugin || function(pluginId) { if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS STUB] uninstallPlugin called for', pluginId); - + if (!confirm(`Are you sure you want to uninstall ${pluginId}?`)) { return Promise.resolve({ cancelled: true }); } - + if (typeof showNotification === 'function') { showNotification(`Uninstalling ${pluginId}...`, 'info'); } - + return fetch('/api/v3/plugins/uninstall', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -201,7 +201,7 @@ window.uninstallPlugin = window.uninstallPlugin || function(pluginId) { // Define configurePlugin early to ensure it's always available window.configurePlugin = window.configurePlugin || async function(pluginId) { if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS STUB] configurePlugin called for', pluginId); - + // Switch to the plugin's configuration tab instead of opening a modal // This matches the behavior of clicking the plugin tab at the top function getAppComponent() { @@ -213,13 +213,13 @@ window.configurePlugin = window.configurePlugin || async function(pluginId) { } return null; } - + const appComponent = getAppComponent(); if (appComponent) { // Set the active tab to the plugin ID appComponent.activeTab = pluginId; if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS STUB] Switched to plugin tab:', pluginId); - + // Scroll to top of page to ensure the tab is visible window.scrollTo({ top: 0, behavior: 'smooth' }); } else { @@ -238,34 +238,34 @@ if (!window._pluginToggleRequests) { // Define togglePlugin early to ensure it's always available window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS STUB] togglePlugin called for', pluginId, 'enabled:', enabled); - + const plugin = (window.installedPlugins || []).find(p => p.id === pluginId); const pluginName = plugin ? (plugin.name || pluginId) : pluginId; const action = enabled ? 'enabling' : 'disabling'; - + // Generate unique token for this toggle request to prevent race conditions const requestToken = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; window._pluginToggleRequests[pluginId] = requestToken; - + // Update UI immediately for better UX const toggleCheckbox = document.getElementById(`toggle-${pluginId}`); const toggleLabel = document.getElementById(`toggle-label-${pluginId}`); const wrapperDiv = toggleCheckbox?.parentElement?.querySelector('.flex.items-center.gap-2'); const toggleTrack = wrapperDiv?.querySelector('.relative.w-14'); const toggleHandle = toggleTrack?.querySelector('.absolute'); - + // Disable checkbox and add disabled class to prevent overlapping requests if (toggleCheckbox) { toggleCheckbox.checked = enabled; toggleCheckbox.disabled = true; toggleCheckbox.classList.add('opacity-50', 'cursor-not-allowed'); } - + // Disable wrapper to provide visual feedback if (wrapperDiv) { wrapperDiv.classList.add('opacity-50', 'pointer-events-none'); } - + // Update wrapper background and border if (wrapperDiv) { if (enabled) { @@ -276,7 +276,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { wrapperDiv.classList.add('bg-gray-50', 'border-gray-300'); } } - + // Update toggle track if (toggleTrack) { if (enabled) { @@ -287,7 +287,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { toggleTrack.classList.add('bg-gray-300'); } } - + // Update toggle handle if (toggleHandle) { if (enabled) { @@ -300,7 +300,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { toggleHandle.innerHTML = ''; } } - + // Update label with icon and text if (toggleLabel) { if (enabled) { @@ -311,7 +311,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { toggleLabel.innerHTML = 'Disabled'; } } - + if (typeof showNotification === 'function') { showNotification(`${action.charAt(0).toUpperCase() + action.slice(1)} ${pluginName}...`, 'info'); } @@ -328,7 +328,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { console.log(`[togglePlugin] Ignoring out-of-order response for ${pluginId}`); return; } - + if (typeof showNotification === 'function') { showNotification(data.message, data.status); } @@ -350,7 +350,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { loadInstalledPlugins(); } } - + // Clear token and re-enable UI delete window._pluginToggleRequests[pluginId]; if (toggleCheckbox) { @@ -367,7 +367,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { console.log(`[togglePlugin] Ignoring out-of-order error for ${pluginId}`); return; } - + if (typeof showNotification === 'function') { showNotification('Error toggling plugin: ' + error.message, 'error'); } @@ -378,7 +378,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) { if (typeof loadInstalledPlugins === 'function') { loadInstalledPlugins(); } - + // Clear token and re-enable UI delete window._pluginToggleRequests[pluginId]; if (toggleCheckbox) { @@ -418,13 +418,13 @@ window.__pluginDomReady = window.__pluginDomReady || false; // Use document-level delegation so it works for dynamically added content const handleGlobalPluginAction = function(event) { // Only handle if it's a plugin action - const button = event.target.closest('button[data-action][data-plugin-id]') || + const button = event.target.closest('button[data-action][data-plugin-id]') || event.target.closest('input[data-action][data-plugin-id]'); if (!button) return; - + const action = button.getAttribute('data-action'); const pluginId = button.getAttribute('data-plugin-id'); - + // For toggle and configure, ensure functions are available if (action === 'toggle' || action === 'configure') { const funcName = action === 'toggle' ? 'togglePlugin' : 'configurePlugin'; @@ -434,12 +434,12 @@ window.__pluginDomReady = window.__pluginDomReady || false; event.stopPropagation(); console.warn(`[GLOBAL DELEGATION] ${funcName} not available yet, waiting...`); - + // Capture state synchronously from plugin data (source of truth) let targetChecked = false; if (action === 'toggle') { const plugin = (window.installedPlugins || []).find(p => p.id === pluginId); - + let currentEnabled; if (plugin) { currentEnabled = Boolean(plugin.enabled); @@ -448,7 +448,7 @@ window.__pluginDomReady = window.__pluginDomReady || false; } else { currentEnabled = false; } - + targetChecked = !currentEnabled; // Toggle to opposite state } @@ -476,11 +476,11 @@ window.__pluginDomReady = window.__pluginDomReady || false; return; // Don't proceed with normal handling } } - + // Prevent default and stop propagation to avoid double handling event.preventDefault(); event.stopPropagation(); - + // If handlePluginAction exists, use it; otherwise handle directly if (typeof handlePluginAction === 'function') { handlePluginAction(event); @@ -489,7 +489,7 @@ window.__pluginDomReady = window.__pluginDomReady || false; if (action === 'toggle' && window.togglePlugin) { // Get the current enabled state from plugin data (source of truth) const plugin = (window.installedPlugins || []).find(p => p.id === pluginId); - + let currentEnabled; if (plugin) { currentEnabled = Boolean(plugin.enabled); @@ -498,17 +498,17 @@ window.__pluginDomReady = window.__pluginDomReady || false; } else { currentEnabled = false; } - + // Toggle the state - we want the opposite of current state const isChecked = !currentEnabled; - + // Prevent default behavior to avoid double-toggling and change event // (Already done at start of function, but safe to repeat) event.preventDefault(); event.stopPropagation(); - + console.log('[DEBUG toggle fallback] Plugin:', pluginId, 'Current enabled (from data):', currentEnabled, 'New state:', isChecked); - + window.togglePlugin(pluginId, isChecked); } else if (action === 'configure' && window.configurePlugin) { event.preventDefault(); @@ -529,7 +529,7 @@ window.__pluginDomReady = window.__pluginDomReady || false; } } }; - + // Set up delegation on document (capture phase for better reliability) document.addEventListener('click', handleGlobalPluginAction, true); document.addEventListener('change', handleGlobalPluginAction, true); @@ -563,7 +563,7 @@ window.attachGithubTokenCollapseHandler = function() { console.warn('[attachGithubTokenCollapseHandler] GitHub token collapse button not found'); return; } - + console.log('[attachGithubTokenCollapseHandler] Checking toggleGithubTokenContent...', { exists: typeof window.toggleGithubTokenContent }); @@ -571,23 +571,23 @@ window.attachGithubTokenCollapseHandler = function() { console.warn('[attachGithubTokenCollapseHandler] toggleGithubTokenContent function not defined'); return; } - + // Remove any existing listeners by cloning the button const parent = toggleTokenCollapseBtn.parentNode; if (!parent) { console.warn('[attachGithubTokenCollapseHandler] Button parent not found'); return; } - + const newBtn = toggleTokenCollapseBtn.cloneNode(true); parent.replaceChild(newBtn, toggleTokenCollapseBtn); - + // Attach listener to the new button newBtn.addEventListener('click', function(e) { console.log('[attachGithubTokenCollapseHandler] Button clicked, calling toggleGithubTokenContent'); window.toggleGithubTokenContent(e); }); - + console.log('[attachGithubTokenCollapseHandler] Handler attached to button:', newBtn.id); }; @@ -595,36 +595,36 @@ window.attachGithubTokenCollapseHandler = function() { console.log('[DEFINE] Defining toggleGithubTokenContent function...'); window.toggleGithubTokenContent = function(e) { console.log('[toggleGithubTokenContent] called', e); - + if (e) { e.stopPropagation(); e.preventDefault(); } - + const tokenContent = document.getElementById('github-token-content'); const tokenIconCollapse = document.getElementById('github-token-icon-collapse'); const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse'); - + console.log('[toggleGithubTokenContent] Elements found:', { tokenContent: !!tokenContent, tokenIconCollapse: !!tokenIconCollapse, toggleTokenCollapseBtn: !!toggleTokenCollapseBtn }); - + if (!tokenContent || !toggleTokenCollapseBtn) { console.warn('[toggleGithubTokenContent] GitHub token content or button not found'); return; } - + const hasHiddenClass = tokenContent.classList.contains('hidden'); const computedDisplay = window.getComputedStyle(tokenContent).display; - + console.log('[toggleGithubTokenContent] Current state:', { hasHiddenClass, computedDisplay, buttonText: toggleTokenCollapseBtn.querySelector('span')?.textContent }); - + if (hasHiddenClass || computedDisplay === 'none') { // Show content - remove hidden class, add block class, remove inline display tokenContent.classList.remove('hidden'); @@ -657,35 +657,35 @@ window.toggleGithubTokenContent = function(e) { console.log('[DEFINE] Defining handleGitHubPluginInstall function...'); window.handleGitHubPluginInstall = function() { console.log('[handleGitHubPluginInstall] Function called!'); - + const urlInput = document.getElementById('github-plugin-url'); const statusDiv = document.getElementById('github-plugin-status'); const branchInput = document.getElementById('plugin-branch-input'); const installBtn = document.getElementById('install-plugin-from-url'); - + if (!urlInput) { console.error('[handleGitHubPluginInstall] URL input not found'); alert('Error: Could not find URL input field'); return; } - + const repoUrl = urlInput.value.trim(); console.log('[handleGitHubPluginInstall] Repo URL:', repoUrl); - + if (!repoUrl) { if (statusDiv) { statusDiv.innerHTML = 'Please enter a GitHub URL'; } return; } - + if (!repoUrl.includes('github.com')) { if (statusDiv) { statusDiv.innerHTML = 'Please enter a valid GitHub URL'; } return; } - + // Disable button and show loading if (installBtn) { installBtn.disabled = true; @@ -694,15 +694,15 @@ window.handleGitHubPluginInstall = function() { if (statusDiv) { statusDiv.innerHTML = 'Installing plugin...'; } - + const branch = branchInput?.value?.trim() || null; const requestBody = { repo_url: repoUrl }; if (branch) { requestBody.branch = branch; } - + console.log('[handleGitHubPluginInstall] Sending request:', requestBody); - + fetch('/api/v3/plugins/install-from-url', { method: 'POST', headers: { @@ -721,12 +721,12 @@ window.handleGitHubPluginInstall = function() { statusDiv.innerHTML = `Successfully installed: ${data.plugin_id}`; } urlInput.value = ''; - + // Show notification if available if (typeof showNotification === 'function') { showNotification(`Plugin ${data.plugin_id} installed successfully`, 'success'); } - + // Refresh installed plugins list if function available setTimeout(() => { if (typeof loadInstalledPlugins === 'function') { @@ -796,38 +796,38 @@ window.checkGitHubAuthStatus = function checkGitHubAuthStatus() { if (!dismissed) { if (warning && rateLimit) { rateLimit.textContent = authData.rate_limit; - + // Update warning message for invalid tokens if (tokenStatus === 'invalid' && authData.error) { const warningText = warning.querySelector('p.text-sm.text-yellow-700'); if (warningText) { // Clear existing content warningText.textContent = ''; - + // Create safe error message with fallback const errorMsg = (authData.message || authData.error || 'Unknown error').toString(); - + // Create element for "Token Invalid:" label const strong = document.createElement('strong'); strong.textContent = 'Token Invalid:'; - + // Create text node for error message and suffix const errorText = document.createTextNode(` ${errorMsg}. Please update your GitHub token to increase API rate limits to 5,000 requests/hour.`); - + // Append elements safely (no innerHTML) warningText.appendChild(strong); warningText.appendChild(errorText); } } // For 'none' status, use the default message from HTML template - + // Show warning using both classList and style.display warning.classList.remove('hidden'); warning.style.display = ''; console.log(`GitHub token status: ${tokenStatus} - showing API limit warning`); } } - + // Ensure settings panel is accessible when token is missing or invalid // Panel can be opened via "Configure Token" link in warning // Don't force it to be visible, but don't prevent it from being shown @@ -839,13 +839,13 @@ window.checkGitHubAuthStatus = function checkGitHubAuthStatus() { warning.style.display = 'none'; console.log('GitHub token is valid - hiding API limit warning'); } - + // Make settings panel visible but collapsed (accessible for token management) if (settings) { // Remove hidden class from panel itself - make it visible using both methods settings.classList.remove('hidden'); settings.style.display = ''; - + // Always collapse the content when token is valid (user must click expand) const tokenContent = document.getElementById('github-token-content'); if (tokenContent) { @@ -854,26 +854,26 @@ window.checkGitHubAuthStatus = function checkGitHubAuthStatus() { tokenContent.classList.remove('block'); tokenContent.style.display = 'none'; } - + // Update collapse button state to show "Expand" const tokenIconCollapse = document.getElementById('github-token-icon-collapse'); if (tokenIconCollapse) { tokenIconCollapse.classList.remove('fa-chevron-up'); tokenIconCollapse.classList.add('fa-chevron-down'); } - + const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse'); if (toggleTokenCollapseBtn) { const span = toggleTokenCollapseBtn.querySelector('span'); if (span) span.textContent = 'Expand'; - + // Ensure event listener is attached if (window.attachGithubTokenCollapseHandler) { window.attachGithubTokenCollapseHandler(); } } } - + // Clear dismissal flag when token becomes valid sessionStorage.removeItem('github-auth-warning-dismissed'); } @@ -889,7 +889,7 @@ window.checkGitHubAuthStatus = function checkGitHubAuthStatus() { 'use strict'; if (_PLUGIN_DEBUG_EARLY) console.log('Plugin manager script starting...'); - + // Local variables for this instance let installedPlugins = []; window.currentPluginConfig = null; @@ -1026,24 +1026,24 @@ window.initPluginsPage = function() { console.log('Plugin page already initialized or initializing, skipping...'); return; } - + // Check if required elements exist const installedGrid = document.getElementById('installed-plugins-grid'); if (!installedGrid) { console.log('Plugin elements not ready yet'); return false; } - + window.pluginManager.initializing = true; window.__pluginDomReady = true; - + // Check GitHub auth status immediately (don't wait for full initialization) // This can run in parallel with other initialization if (window.checkGitHubAuthStatus) { console.log('[INIT] Checking GitHub auth status immediately...'); window.checkGitHubAuthStatus(); } - + // If we fetched data before the DOM existed, render it now if (window.__pendingInstalledPlugins) { console.log('[RENDER] Applying pending installed plugins data'); @@ -1059,7 +1059,7 @@ window.initPluginsPage = function() { } initializePlugins(); - + // Event listeners (remove old ones first to prevent duplicates) const refreshBtn = document.getElementById('refresh-plugins-btn'); const updateAllBtn = document.getElementById('update-all-plugins-btn'); @@ -1092,14 +1092,14 @@ window.initPluginsPage = function() { if (closeBtn) { closeBtn.replaceWith(closeBtn.cloneNode(true)); document.getElementById('close-plugin-config').addEventListener('click', closePluginConfigModal); - + // View toggle buttons document.getElementById('view-toggle-form')?.addEventListener('click', () => switchPluginConfigView('form')); document.getElementById('view-toggle-json')?.addEventListener('click', () => switchPluginConfigView('json')); - + // Reset to defaults button document.getElementById('reset-to-defaults-btn')?.addEventListener('click', resetPluginConfigToDefaults); - + // JSON editor save button document.getElementById('save-json-config-btn')?.addEventListener('click', saveConfigFromJsonEditor); } @@ -1122,7 +1122,7 @@ window.initPluginsPage = function() { // Load on-demand status silently (false = don't show notification) loadOnDemandStatus(false); startOnDemandStatusPolling(); - + window.pluginManager.initialized = true; window.pluginManager.initializing = false; return true; @@ -1147,7 +1147,7 @@ function initializePluginPageWhenReady() { // Try immediate initialization initializePluginPageWhenReady(); } - + // Strategy 1: Immediate check (for direct page loads) if (document.readyState === 'complete' || document.readyState === 'interactive') { // DOM is already ready, try immediately with a small delay to ensure scripts are loaded @@ -1158,7 +1158,7 @@ function initializePluginPageWhenReady() { initTimer = setTimeout(attemptInit, 50); }); } - + // Strategy 3: HTMX afterSwap event (for HTMX-loaded content) // This is the primary way plugins content is loaded // Register unconditionally — HTMX may load after this script (loaded dynamically from CDN) @@ -1197,7 +1197,7 @@ function initializePlugins() { return; } pluginsInitialized = true; - + console.log('[initializePlugins] Starting initialization...'); pluginLog('[INIT] Initializing plugins...'); @@ -1232,7 +1232,7 @@ function initializePlugins() { // Setup search functionality (with guard against duplicate listeners) const searchInput = document.getElementById('plugin-search'); const categorySelect = document.getElementById('plugin-category'); - + if (searchInput && !searchInput._listenerSetup) { searchInput._listenerSetup = true; searchInput.addEventListener('input', debounce(searchPluginStore, 300)); @@ -1241,7 +1241,7 @@ function initializePlugins() { categorySelect._listenerSetup = true; categorySelect.addEventListener('change', searchPluginStore); } - + // Setup GitHub installation handlers console.log('[initializePlugins] About to call setupGitHubInstallHandlers...'); if (typeof setupGitHubInstallHandlers === 'function') { @@ -1251,10 +1251,10 @@ function initializePlugins() { } else { console.error('[initializePlugins] ERROR: setupGitHubInstallHandlers is not a function! Type:', typeof setupGitHubInstallHandlers); } - + // Setup collapsible section handlers setupCollapsibleSections(); - + // Load saved repositories loadSavedRepositories(); @@ -1322,29 +1322,29 @@ function loadInstalledPlugins(forceRefresh = false) { if (data.status === 'success') { const pluginsData = data.data?.plugins; installedPlugins = Array.isArray(pluginsData) ? pluginsData : []; - + // Update cache pluginLoadCache.data = installedPlugins; pluginLoadCache.timestamp = Date.now(); - + // Always update window.installedPlugins to ensure Alpine component can detect changes window.installedPlugins = installedPlugins; - + // Dispatch event to notify Alpine component to update tabs document.dispatchEvent(new CustomEvent('pluginsUpdated', { detail: { plugins: installedPlugins } })); pluginLog('[FETCH] Dispatched pluginsUpdated event with', installedPlugins.length, 'plugins'); - + pluginLog('[FETCH] Loaded', installedPlugins.length, 'plugins'); - + // Debug logging only when enabled if (PLUGIN_DEBUG) { installedPlugins.forEach(plugin => { console.log(`[DEBUG] Plugin ${plugin.id}: enabled=${plugin.enabled}`); }); } - + renderInstalledPlugins(installedPlugins); // Update count @@ -1393,17 +1393,17 @@ function renderInstalledPlugins(plugins) { window.__pendingInstalledPlugins = plugins; return; } - + // Always update window.installedPlugins to ensure Alpine component reactivity window.installedPlugins = plugins; pluginLog('[RENDER] Set window.installedPlugins to:', plugins.length, 'plugins'); - + // Dispatch event to notify Alpine component to update tabs document.dispatchEvent(new CustomEvent('pluginsUpdated', { detail: { plugins: plugins } })); pluginLog('[RENDER] Dispatched pluginsUpdated event'); - + // Also try direct Alpine update as fallback if (window.Alpine && document.querySelector('[x-data="app()"]')) { const appElement = document.querySelector('[x-data="app()"]'); @@ -1433,7 +1433,7 @@ function renderInstalledPlugins(plugins) { const escapeAttr = (text) => { return (text || '').replace(/'/g, "\\'").replace(/"/g, '"'); }; - + // Helper function to escape for JavaScript strings (use JSON.stringify for proper escaping) // JSON.stringify returns a quoted string, so we can use it directly in JavaScript const escapeJs = (text) => { @@ -1443,15 +1443,15 @@ function renderInstalledPlugins(plugins) { container.innerHTML = plugins.map(plugin => { // Convert enabled to boolean for consistent rendering const enabledBool = Boolean(plugin.enabled); - + // Debug: Log enabled status during rendering (only when debug enabled) if (PLUGIN_DEBUG) { console.log(`[DEBUG RENDER] Plugin ${plugin.id}: enabled=${enabledBool}`); } - + // Escape plugin ID for use in HTML attributes and JavaScript const escapedPluginId = escapeAttr(plugin.id); - + return `
@@ -1471,8 +1471,8 @@ function renderInstalledPlugins(plugins) {