From bca4c40ec14d06e062afc0451612e571bb6dda3a Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 14:45:10 -0500 Subject: [PATCH 01/20] fix(plugins): Fix GitHub install button for single plugin installation - Clone install button before attaching event listener to prevent duplicate handlers - Add safety checks for pluginStatusDiv element - Move installFromCustomRegistry function definition earlier in file - Add error logging when button/elements not found - Ensure consistent button reference usage in event handlers Fixes issue where Install button in 'Install Single Plugin' section was not working properly. --- .gitmodules | 3 + plugins/7-segment-clock | 1 + web_interface/static/v3/plugins_manager.js | 214 ++++++++++++--------- 3 files changed, 123 insertions(+), 95 deletions(-) create mode 160000 plugins/7-segment-clock diff --git a/.gitmodules b/.gitmodules index c911c51bd..241438552 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,6 @@ [submodule "plugins/ledmatrix-of-the-day"] path = plugins/ledmatrix-of-the-day url = https://github.com/ChuckBuilds/ledmatrix-of-the-day.git +[submodule "plugins/7-segment-clock"] + path = plugins/7-segment-clock + url = https://github.com/ChuckBuilds/ledmatrix-7-segment-clock diff --git a/plugins/7-segment-clock b/plugins/7-segment-clock new file mode 160000 index 000000000..06782dbeb --- /dev/null +++ b/plugins/7-segment-clock @@ -0,0 +1 @@ +Subproject commit 06782dbebdffe99d9ea5218eaa5791ae913e66da diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 3aae8300c..2a8a847e7 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -4474,6 +4474,48 @@ window.installPlugin = function(pluginId, branch = null) { }); } +window.installFromCustomRegistry = function(pluginId, registryUrl, pluginPath, branch = null) { + const repoUrl = registryUrl; + const requestBody = { + repo_url: repoUrl, + plugin_id: pluginId, + plugin_path: pluginPath + }; + if (branch) { + requestBody.branch = branch; + } + + fetch('/api/v3/plugins/install-from-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + showSuccess(`Plugin ${data.plugin_id} installed successfully`); + // Refresh installed plugins and re-render custom registry + loadInstalledPlugins(); + // Re-render custom registry to update install buttons + const registryUrlInput = document.getElementById('github-registry-url'); + if (registryUrlInput && registryUrlInput.value.trim()) { + document.getElementById('load-registry-from-url').click(); + } + } else { + showError(data.message || 'Installation failed'); + } + }) + .catch(error => { + let errorMsg = 'Error installing plugin: ' + error.message; + if (error.message && error.message.includes('Failed to Fetch')) { + errorMsg += ' - Please try refreshing your browser.'; + } + showError(errorMsg); + }); +} + function setupCollapsibleSections() { console.log('[setupCollapsibleSections] Setting up collapsible sections...'); @@ -4640,63 +4682,87 @@ function setupGitHubInstallHandlers() { const pluginStatusDiv = document.getElementById('github-plugin-status'); if (installBtn && pluginUrlInput) { - installBtn.addEventListener('click', function() { - const repoUrl = pluginUrlInput.value.trim(); - if (!repoUrl) { - pluginStatusDiv.innerHTML = 'Please enter a GitHub URL'; - return; - } - - if (!repoUrl.includes('github.com')) { - pluginStatusDiv.innerHTML = 'Please enter a valid GitHub URL'; - return; - } - - installBtn.disabled = true; - installBtn.innerHTML = 'Installing...'; - pluginStatusDiv.innerHTML = 'Installing plugin...'; + // Clone button to remove any existing listeners (prevents duplicate handlers) + const parent = installBtn.parentNode; + if (parent) { + const newBtn = installBtn.cloneNode(true); + parent.replaceChild(newBtn, installBtn); - const branch = document.getElementById('plugin-branch-input')?.value?.trim() || null; - const requestBody = { repo_url: repoUrl }; - if (branch) { - requestBody.branch = branch; - } + newBtn.addEventListener('click', function() { + const repoUrl = pluginUrlInput.value.trim(); + if (!repoUrl) { + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = 'Please enter a GitHub URL'; + } + return; + } + + if (!repoUrl.includes('github.com')) { + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = 'Please enter a valid GitHub URL'; + } + return; + } + + newBtn.disabled = true; + newBtn.innerHTML = 'Installing...'; + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = 'Installing plugin...'; + } + + const branch = document.getElementById('plugin-branch-input')?.value?.trim() || null; + const requestBody = { repo_url: repoUrl }; + if (branch) { + requestBody.branch = branch; + } + + fetch('/api/v3/plugins/install-from-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = `Successfully installed: ${data.plugin_id}`; + } + pluginUrlInput.value = ''; + + // Refresh installed plugins list + setTimeout(() => { + loadInstalledPlugins(); + }, 1000); + } else { + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = `${data.message || 'Installation failed'}`; + } + } + }) + .catch(error => { + if (pluginStatusDiv) { + pluginStatusDiv.innerHTML = `Error: ${error.message}`; + } + }) + .finally(() => { + newBtn.disabled = false; + newBtn.innerHTML = 'Install'; + }); + }); - fetch('/api/v3/plugins/install-from-url', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - pluginStatusDiv.innerHTML = `Successfully installed: ${data.plugin_id}`; - pluginUrlInput.value = ''; - - // Refresh installed plugins list - setTimeout(() => { - loadInstalledPlugins(); - }, 1000); - } else { - pluginStatusDiv.innerHTML = `${data.message || 'Installation failed'}`; + // Allow Enter key to trigger install + pluginUrlInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + newBtn.click(); } - }) - .catch(error => { - pluginStatusDiv.innerHTML = `Error: ${error.message}`; - }) - .finally(() => { - installBtn.disabled = false; - installBtn.innerHTML = 'Install'; }); - }); - - // Allow Enter key to trigger install - pluginUrlInput.addEventListener('keypress', function(e) { - if (e.key === 'Enter') { - installBtn.click(); - } + } + } else { + console.warn('[setupGitHubInstallHandlers] Install button or URL input not found:', { + installBtn: !!installBtn, + pluginUrlInput: !!pluginUrlInput }); } @@ -4878,48 +4944,6 @@ function renderCustomRegistryPlugins(plugins, registryUrl) { }).join(''); } -window.installFromCustomRegistry = function(pluginId, registryUrl, pluginPath, branch = null) { - const repoUrl = registryUrl; - const requestBody = { - repo_url: repoUrl, - plugin_id: pluginId, - plugin_path: pluginPath - }; - if (branch) { - requestBody.branch = branch; - } - - fetch('/api/v3/plugins/install-from-url', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - showSuccess(`Plugin ${data.plugin_id} installed successfully`); - // Refresh installed plugins and re-render custom registry - loadInstalledPlugins(); - // Re-render custom registry to update install buttons - const registryUrlInput = document.getElementById('github-registry-url'); - if (registryUrlInput && registryUrlInput.value.trim()) { - document.getElementById('load-registry-from-url').click(); - } - } else { - showError(data.message || 'Installation failed'); - } - }) - .catch(error => { - let errorMsg = 'Error installing plugin: ' + error.message; - if (error.message && error.message.includes('Failed to Fetch')) { - errorMsg += ' - Please try refreshing your browser.'; - } - showError(errorMsg); - }); -} - function showSuccess(message) { // Try to use notification system if available, otherwise use alert if (typeof showNotification === 'function') { From 485f51e190c65c0331c2d26d5ba642171708cc87 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 14:51:04 -0500 Subject: [PATCH 02/20] fix(plugins): Add button type and better logging for install button - Add type='button' to install button to prevent form submission - Add console logging to debug click handler attachment - Add preventDefault and stopPropagation to click handler - Improve error logging for debugging --- web_interface/static/v3/plugins_manager.js | 22 ++++++++++++++++++- .../templates/v3/partials/plugins.html | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 2a8a847e7..71b4f694b 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -4681,14 +4681,28 @@ function setupGitHubInstallHandlers() { const pluginUrlInput = document.getElementById('github-plugin-url'); const pluginStatusDiv = document.getElementById('github-plugin-status'); + console.log('[setupGitHubInstallHandlers] Install button elements:', { + installBtn: !!installBtn, + pluginUrlInput: !!pluginUrlInput, + pluginStatusDiv: !!pluginStatusDiv + }); + if (installBtn && pluginUrlInput) { // Clone button to remove any existing listeners (prevents duplicate handlers) const parent = installBtn.parentNode; if (parent) { const newBtn = installBtn.cloneNode(true); + // Ensure button type is set to prevent form submission + newBtn.type = 'button'; parent.replaceChild(newBtn, installBtn); - newBtn.addEventListener('click', function() { + console.log('[setupGitHubInstallHandlers] Install button cloned and replaced, type:', newBtn.type); + + newBtn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('[setupGitHubInstallHandlers] Install button clicked!'); + const repoUrl = pluginUrlInput.value.trim(); if (!repoUrl) { if (pluginStatusDiv) { @@ -4755,9 +4769,15 @@ function setupGitHubInstallHandlers() { // Allow Enter key to trigger install pluginUrlInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { + e.preventDefault(); + console.log('[setupGitHubInstallHandlers] Enter key pressed, triggering install'); newBtn.click(); } }); + + console.log('[setupGitHubInstallHandlers] Install button handler attached successfully'); + } else { + console.error('[setupGitHubInstallHandlers] Install button parent not found!'); } } else { console.warn('[setupGitHubInstallHandlers] Install button or URL input not found:', { diff --git a/web_interface/templates/v3/partials/plugins.html b/web_interface/templates/v3/partials/plugins.html index e6ea736b0..bf6413db5 100644 --- a/web_interface/templates/v3/partials/plugins.html +++ b/web_interface/templates/v3/partials/plugins.html @@ -221,7 +221,7 @@

- From 211142011e859ea2a81078b0acecc49a57d03d9d Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:02:18 -0500 Subject: [PATCH 03/20] fix(plugins): Re-attach install button handler when section is shown - Extract install button handler to separate function - Re-attach handler when GitHub install section is toggled visible - Add data attribute to prevent duplicate handler attachments - Add comprehensive logging for debugging - Handler now attaches even if section starts hidden --- web_interface/static/v3/plugins_manager.js | 169 ++++++++++++--------- 1 file changed, 97 insertions(+), 72 deletions(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 71b4f694b..745443691 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -4613,95 +4613,41 @@ window.removeSavedRepository = function(repoUrl) { }); } -function setupGitHubInstallHandlers() { - console.log('[setupGitHubInstallHandlers] Setting up GitHub install handlers...'); - - // Toggle GitHub install section visibility - const toggleBtn = document.getElementById('toggle-github-install'); - const installSection = document.getElementById('github-install-section'); - const icon = document.getElementById('github-install-icon'); - - console.log('[setupGitHubInstallHandlers] Elements found:', { - button: !!toggleBtn, - section: !!installSection, - icon: !!icon - }); - - if (toggleBtn && installSection) { - // Clone button to remove any existing listeners - const parent = toggleBtn.parentNode; - if (parent) { - const newBtn = toggleBtn.cloneNode(true); - parent.replaceChild(newBtn, toggleBtn); - - newBtn.addEventListener('click', function(e) { - e.stopPropagation(); - e.preventDefault(); - console.log('[setupGitHubInstallHandlers] GitHub install toggle clicked'); - - const section = document.getElementById('github-install-section'); - const iconEl = document.getElementById('github-install-icon'); - const btn = document.getElementById('toggle-github-install'); - - if (!section || !btn) return; - - const hasHiddenClass = section.classList.contains('hidden'); - const computedDisplay = window.getComputedStyle(section).display; - - if (hasHiddenClass || computedDisplay === 'none') { - // Show section - remove hidden, ensure visible - section.classList.remove('hidden'); - section.style.removeProperty('display'); - if (iconEl) { - iconEl.classList.remove('fa-chevron-down'); - iconEl.classList.add('fa-chevron-up'); - } - const span = btn.querySelector('span'); - if (span) span.textContent = 'Hide'; - } else { - // Hide section - add hidden, set display none - section.classList.add('hidden'); - section.style.display = 'none'; - if (iconEl) { - iconEl.classList.remove('fa-chevron-up'); - iconEl.classList.add('fa-chevron-down'); - } - const span = btn.querySelector('span'); - if (span) span.textContent = 'Show'; - } - }); - console.log('[setupGitHubInstallHandlers] Handler attached'); - } - } else { - console.warn('[setupGitHubInstallHandlers] Required elements not found'); - } - - // Install single plugin from URL +// Separate function to attach install button handler (can be called multiple times) +function attachInstallButtonHandler() { const installBtn = document.getElementById('install-plugin-from-url'); const pluginUrlInput = document.getElementById('github-plugin-url'); const pluginStatusDiv = document.getElementById('github-plugin-status'); - console.log('[setupGitHubInstallHandlers] Install button elements:', { + console.log('[attachInstallButtonHandler] Looking for install button elements:', { installBtn: !!installBtn, pluginUrlInput: !!pluginUrlInput, pluginStatusDiv: !!pluginStatusDiv }); if (installBtn && pluginUrlInput) { + // Check if handler already attached (prevent duplicates) + if (installBtn.hasAttribute('data-handler-attached')) { + console.log('[attachInstallButtonHandler] Handler already attached, skipping'); + return; + } + // Clone button to remove any existing listeners (prevents duplicate handlers) const parent = installBtn.parentNode; if (parent) { const newBtn = installBtn.cloneNode(true); // Ensure button type is set to prevent form submission newBtn.type = 'button'; + // Mark as having handler attached + newBtn.setAttribute('data-handler-attached', 'true'); parent.replaceChild(newBtn, installBtn); - console.log('[setupGitHubInstallHandlers] Install button cloned and replaced, type:', newBtn.type); + console.log('[attachInstallButtonHandler] Install button cloned and replaced, type:', newBtn.type); newBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); - console.log('[setupGitHubInstallHandlers] Install button clicked!'); + console.log('[attachInstallButtonHandler] Install button clicked!'); const repoUrl = pluginUrlInput.value.trim(); if (!repoUrl) { @@ -4730,6 +4676,8 @@ function setupGitHubInstallHandlers() { requestBody.branch = branch; } + console.log('[attachInstallButtonHandler] Sending install request:', requestBody); + fetch('/api/v3/plugins/install-from-url', { method: 'POST', headers: { @@ -4737,8 +4685,12 @@ function setupGitHubInstallHandlers() { }, body: JSON.stringify(requestBody) }) - .then(response => response.json()) + .then(response => { + console.log('[attachInstallButtonHandler] Response status:', response.status); + return response.json(); + }) .then(data => { + console.log('[attachInstallButtonHandler] Response data:', data); if (data.status === 'success') { if (pluginStatusDiv) { pluginStatusDiv.innerHTML = `Successfully installed: ${data.plugin_id}`; @@ -4756,6 +4708,7 @@ function setupGitHubInstallHandlers() { } }) .catch(error => { + console.error('[attachInstallButtonHandler] Error:', error); if (pluginStatusDiv) { pluginStatusDiv.innerHTML = `Error: ${error.message}`; } @@ -4770,21 +4723,93 @@ function setupGitHubInstallHandlers() { pluginUrlInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { e.preventDefault(); - console.log('[setupGitHubInstallHandlers] Enter key pressed, triggering install'); + console.log('[attachInstallButtonHandler] Enter key pressed, triggering install'); newBtn.click(); } }); - console.log('[setupGitHubInstallHandlers] Install button handler attached successfully'); + console.log('[attachInstallButtonHandler] Install button handler attached successfully'); } else { - console.error('[setupGitHubInstallHandlers] Install button parent not found!'); + console.error('[attachInstallButtonHandler] Install button parent not found!'); } } else { - console.warn('[setupGitHubInstallHandlers] Install button or URL input not found:', { + console.warn('[attachInstallButtonHandler] Install button or URL input not found:', { installBtn: !!installBtn, pluginUrlInput: !!pluginUrlInput }); } +} + +function setupGitHubInstallHandlers() { + console.log('[setupGitHubInstallHandlers] Setting up GitHub install handlers...'); + + // Toggle GitHub install section visibility + const toggleBtn = document.getElementById('toggle-github-install'); + const installSection = document.getElementById('github-install-section'); + const icon = document.getElementById('github-install-icon'); + + console.log('[setupGitHubInstallHandlers] Elements found:', { + button: !!toggleBtn, + section: !!installSection, + icon: !!icon + }); + + if (toggleBtn && installSection) { + // Clone button to remove any existing listeners + const parent = toggleBtn.parentNode; + if (parent) { + const newBtn = toggleBtn.cloneNode(true); + parent.replaceChild(newBtn, toggleBtn); + + newBtn.addEventListener('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + console.log('[setupGitHubInstallHandlers] GitHub install toggle clicked'); + + const section = document.getElementById('github-install-section'); + const iconEl = document.getElementById('github-install-icon'); + const btn = document.getElementById('toggle-github-install'); + + if (!section || !btn) return; + + const hasHiddenClass = section.classList.contains('hidden'); + const computedDisplay = window.getComputedStyle(section).display; + + if (hasHiddenClass || computedDisplay === 'none') { + // Show section - remove hidden, ensure visible + section.classList.remove('hidden'); + section.style.removeProperty('display'); + if (iconEl) { + iconEl.classList.remove('fa-chevron-down'); + iconEl.classList.add('fa-chevron-up'); + } + const span = btn.querySelector('span'); + if (span) span.textContent = 'Hide'; + + // Re-attach install button handler when section is shown (in case elements weren't ready before) + setTimeout(() => { + attachInstallButtonHandler(); + }, 100); + } else { + // Hide section - add hidden, set display none + section.classList.add('hidden'); + section.style.display = 'none'; + if (iconEl) { + iconEl.classList.remove('fa-chevron-up'); + iconEl.classList.add('fa-chevron-down'); + } + const span = btn.querySelector('span'); + if (span) span.textContent = 'Show'; + } + }); + console.log('[setupGitHubInstallHandlers] Handler attached'); + } + } else { + console.warn('[setupGitHubInstallHandlers] Required elements not found'); + } + + // Install single plugin from URL - use separate function so we can re-call it + attachInstallButtonHandler(); // Load registry from URL const loadRegistryBtn = document.getElementById('load-registry-from-url'); From 1f0f155e015aa25b534eb6bc595df6945c04ee2d Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:07:45 -0500 Subject: [PATCH 04/20] fix(plugins): Add comprehensive logging to debug install button handler - Add logging at function entry points - Add logging when section is shown and handler re-attached - Add logging before and after calling attachInstallButtonHandler - Helps diagnose why handler isn't being attached --- web_interface/static/v3/plugins_manager.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 745443691..b06fff3b2 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -4615,6 +4615,7 @@ window.removeSavedRepository = function(repoUrl) { // Separate function to attach install button handler (can be called multiple times) function attachInstallButtonHandler() { + console.log('[attachInstallButtonHandler] FUNCTION CALLED'); const installBtn = document.getElementById('install-plugin-from-url'); const pluginUrlInput = document.getElementById('github-plugin-url'); const pluginStatusDiv = document.getElementById('github-plugin-status'); @@ -4741,7 +4742,7 @@ function attachInstallButtonHandler() { } function setupGitHubInstallHandlers() { - console.log('[setupGitHubInstallHandlers] Setting up GitHub install handlers...'); + console.log('[setupGitHubInstallHandlers] FUNCTION CALLED - Setting up GitHub install handlers...'); // Toggle GitHub install section visibility const toggleBtn = document.getElementById('toggle-github-install'); @@ -4787,7 +4788,9 @@ function setupGitHubInstallHandlers() { if (span) span.textContent = 'Hide'; // Re-attach install button handler when section is shown (in case elements weren't ready before) + console.log('[setupGitHubInstallHandlers] Section shown, will re-attach install button handler in 100ms'); setTimeout(() => { + console.log('[setupGitHubInstallHandlers] Re-attaching install button handler now'); attachInstallButtonHandler(); }, 100); } else { @@ -4809,7 +4812,9 @@ function setupGitHubInstallHandlers() { } // Install single plugin from URL - use separate function so we can re-call it + console.log('[setupGitHubInstallHandlers] About to call attachInstallButtonHandler...'); attachInstallButtonHandler(); + console.log('[setupGitHubInstallHandlers] Called attachInstallButtonHandler'); // Load registry from URL const loadRegistryBtn = document.getElementById('load-registry-from-url'); From 1c31e8bec3487730d4a9162220ef4734b1406295 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:10:05 -0500 Subject: [PATCH 05/20] fix(plugins): Expose GitHub install handlers globally and add fallback - Expose setupGitHubInstallHandlers and attachInstallButtonHandler to window object - Add fallback handler attachment after page load delay - Fix typo in getElementById call - Allows manual testing from browser console - Ensures handlers are accessible even if IIFE scope issues occur --- web_interface/static/v3/plugins_manager.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index b06fff3b2..d870bbac4 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -6155,6 +6155,15 @@ if (typeof loadInstalledPlugins !== 'undefined') { if (typeof renderInstalledPlugins !== 'undefined') { window.renderInstalledPlugins = renderInstalledPlugins; } +// Expose GitHub install handlers for debugging and manual testing +if (typeof setupGitHubInstallHandlers !== 'undefined') { + window.setupGitHubInstallHandlers = setupGitHubInstallHandlers; + console.log('[GLOBAL] setupGitHubInstallHandlers exposed to window'); +} +if (typeof attachInstallButtonHandler !== 'undefined') { + window.attachInstallButtonHandler = attachInstallButtonHandler; + console.log('[GLOBAL] attachInstallButtonHandler exposed to window'); +} // searchPluginStore is now exposed inside the IIFE after its definition // Verify critical functions are available From 218a5edbfcd5568e0a175750e61db88fa774dad5 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:10:22 -0500 Subject: [PATCH 06/20] fix(plugins): Add fallback handler attachment after page load --- web_interface/static/v3/plugins_manager.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index d870bbac4..01d2c1f73 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -6208,5 +6208,15 @@ setTimeout(function() { } else { console.log('installed-plugins-grid not found yet, will retry via event listeners'); } + + // Also try to attach install button handler after a delay (fallback) + setTimeout(() => { + if (typeof window.attachInstallButtonHandler === 'function') { + console.log('[FALLBACK] Attempting to attach install button handler...'); + window.attachInstallButtonHandler(); + } else { + console.warn('[FALLBACK] attachInstallButtonHandler not available on window'); + } + }, 500); }, 200); From f132bfd1c94a8839b6c7e19081e1674f6f0b830c Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:16:35 -0500 Subject: [PATCH 07/20] fix(plugins): Ensure GitHub install handlers are set up even if already initialized - Add check to verify setupGitHubInstallHandlers exists before calling - Call setupGitHubInstallHandlers even if initializePlugins was already called - Add comprehensive logging to track function execution - Helps diagnose why handlers aren't being attached --- web_interface/static/v3/plugins_manager.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 01d2c1f73..079b7e6b9 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -935,11 +935,17 @@ function initializePluginPageWhenReady() { let pluginsInitialized = false; function initializePlugins() { - console.log('[initializePlugins] Called, pluginsInitialized:', pluginsInitialized); + console.log('[initializePlugins] FUNCTION CALLED, pluginsInitialized:', pluginsInitialized); // Guard against multiple initializations if (pluginsInitialized) { - console.log('[initializePlugins] Already initialized, skipping'); - pluginLog('[INIT] Plugins already initialized, skipping'); + console.log('[initializePlugins] Already initialized, skipping (but still setting up handlers)'); + // Still set up handlers even if already initialized (in case page was HTMX swapped) + console.log('[initializePlugins] Force setting up GitHub handlers anyway...'); + if (typeof setupGitHubInstallHandlers === 'function') { + setupGitHubInstallHandlers(); + } else { + console.error('[initializePlugins] setupGitHubInstallHandlers not found!'); + } return; } pluginsInitialized = true; @@ -981,7 +987,14 @@ function initializePlugins() { } // Setup GitHub installation handlers - setupGitHubInstallHandlers(); + console.log('[initializePlugins] About to call setupGitHubInstallHandlers...'); + if (typeof setupGitHubInstallHandlers === 'function') { + console.log('[initializePlugins] setupGitHubInstallHandlers is a function, calling it...'); + setupGitHubInstallHandlers(); + console.log('[initializePlugins] setupGitHubInstallHandlers called'); + } else { + console.error('[initializePlugins] ERROR: setupGitHubInstallHandlers is not a function! Type:', typeof setupGitHubInstallHandlers); + } // Setup collapsible section handlers setupCollapsibleSections(); From 4c93c4a25d198808081675561adca1d0c6808afc Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:16:57 -0500 Subject: [PATCH 08/20] fix(plugins): Add more prominent logging markers for easier debugging --- web_interface/static/v3/plugins_manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 079b7e6b9..39079731a 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -4628,7 +4628,7 @@ window.removeSavedRepository = function(repoUrl) { // Separate function to attach install button handler (can be called multiple times) function attachInstallButtonHandler() { - console.log('[attachInstallButtonHandler] FUNCTION CALLED'); + console.log('[attachInstallButtonHandler] ===== FUNCTION CALLED ====='); const installBtn = document.getElementById('install-plugin-from-url'); const pluginUrlInput = document.getElementById('github-plugin-url'); const pluginStatusDiv = document.getElementById('github-plugin-status'); @@ -4755,7 +4755,7 @@ function attachInstallButtonHandler() { } function setupGitHubInstallHandlers() { - console.log('[setupGitHubInstallHandlers] FUNCTION CALLED - Setting up GitHub install handlers...'); + console.log('[setupGitHubInstallHandlers] ===== FUNCTION CALLED ===== Setting up GitHub install handlers...'); // Toggle GitHub install section visibility const toggleBtn = document.getElementById('toggle-github-install'); From 063d17e3ff4f580fb2fce29c3e89b9f74d8311f1 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:17:53 -0500 Subject: [PATCH 09/20] fix(plugins): Add simple standalone handler for GitHub plugin installation - Create handleGitHubPluginInstall() function defined early and globally - Add inline onclick handler to button as fallback - Bypasses complex initialization flow and IIFE scope issues - Direct approach that works immediately without dependencies - Provides clear error messages and logging --- web_interface/static/v3/plugins_manager.js | 110 ++++++++++++++++++ .../templates/v3/partials/plugins.html | 4 +- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 39079731a..4112889fa 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -548,6 +548,116 @@ window.toggleGithubTokenContent = function(e) { } }; +// Simple standalone handler for GitHub plugin installation +// Defined early and globally to ensure it's always available +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; + installBtn.innerHTML = 'Installing...'; + } + 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: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }) + .then(response => { + console.log('[handleGitHubPluginInstall] Response status:', response.status); + return response.json(); + }) + .then(data => { + console.log('[handleGitHubPluginInstall] Response data:', data); + if (data.status === 'success') { + if (statusDiv) { + 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') { + loadInstalledPlugins(); + } else if (typeof window.loadInstalledPlugins === 'function') { + window.loadInstalledPlugins(); + } + }, 1000); + } else { + if (statusDiv) { + statusDiv.innerHTML = `${data.message || 'Installation failed'}`; + } + if (typeof showNotification === 'function') { + showNotification(data.message || 'Installation failed', 'error'); + } + } + }) + .catch(error => { + console.error('[handleGitHubPluginInstall] Error:', error); + if (statusDiv) { + statusDiv.innerHTML = `Error: ${error.message}`; + } + if (typeof showNotification === 'function') { + showNotification('Error installing plugin: ' + error.message, 'error'); + } + }) + .finally(() => { + if (installBtn) { + installBtn.disabled = false; + installBtn.innerHTML = 'Install'; + } + }); +}; +console.log('[DEFINE] handleGitHubPluginInstall defined and ready'); + // GitHub Authentication Status - Define early so it's available in IIFE // Shows warning banner only when token is missing or invalid // The token itself is never exposed to the frontend for security diff --git a/web_interface/templates/v3/partials/plugins.html b/web_interface/templates/v3/partials/plugins.html index bf6413db5..c77eb9e42 100644 --- a/web_interface/templates/v3/partials/plugins.html +++ b/web_interface/templates/v3/partials/plugins.html @@ -221,7 +221,9 @@

- From abc4d48d1c1ae6c695837c62f1cf60736e29fcd0 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:24:12 -0500 Subject: [PATCH 10/20] chore: Update 7-segment-clock plugin submodule - Update to latest version with scaling support - Includes compatible_versions field fix for plugin store installation --- plugins/7-segment-clock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/7-segment-clock b/plugins/7-segment-clock index 06782dbeb..7e9c61003 160000 --- a/plugins/7-segment-clock +++ b/plugins/7-segment-clock @@ -1 +1 @@ -Subproject commit 06782dbebdffe99d9ea5218eaa5791ae913e66da +Subproject commit 7e9c610033a2fa75603e600743f43404f009b0d4 From 71340fd074e1ac27d73196a2e668eede6ca41e62 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 15:53:31 -0500 Subject: [PATCH 11/20] fix(plugins): Add update and uninstall handling to global event delegation fallback - Add 'update' action handling in handleGlobalPluginAction fallback - Add 'uninstall' action handling with confirmation dialog - Fixes issue where update/uninstall buttons did nothing - Buttons now work even if handlePluginAction isn't available yet --- web_interface/static/v3/plugins_manager.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 4112889fa..353714e8f 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -284,6 +284,18 @@ window.__pluginDomReady = window.__pluginDomReady || false; event.preventDefault(); event.stopPropagation(); window.configurePlugin(pluginId); + } else if (action === 'update' && window.updatePlugin) { + event.preventDefault(); + event.stopPropagation(); + console.log('[DEBUG update fallback] Updating plugin:', pluginId); + window.updatePlugin(pluginId); + } else if (action === 'uninstall' && window.uninstallPlugin) { + event.preventDefault(); + event.stopPropagation(); + console.log('[DEBUG uninstall fallback] Uninstalling plugin:', pluginId); + if (confirm(`Are you sure you want to uninstall ${pluginId}?`)) { + window.uninstallPlugin(pluginId); + } } } }; From 8306490d23c6cfd872468f48dee043590ed4fe77 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 16:06:14 -0500 Subject: [PATCH 12/20] fix(plugins): Improve error message for plugin updates from GitHub URLs - Check if plugin is a git repository before checking registry - Provide more accurate error messages for plugins installed from URLs - Fixes misleading 'Plugin not found in registry' error for git-based plugins - Update should work for plugins installed from GitHub URLs even if not in registry --- web_interface/blueprints/api_v3.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 281bd4ff3..06d61b092 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -2259,9 +2259,18 @@ def update_plugin(): if not plugin_path_dir.exists(): error_msg += ': Plugin not found' else: - plugin_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id) - if not plugin_info: - error_msg += ': Plugin not found in registry' + # Check if it's a git repo (could be installed from URL, not in registry) + git_info = api_v3.plugin_store_manager._get_local_git_info(plugin_path_dir) + if git_info: + # It's a git repo, so update should have worked - provide generic error + error_msg += ': Update failed (check logs for details)' + else: + # Not a git repo, check if it's in registry + plugin_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id) + if not plugin_info: + error_msg += ': Plugin not found in registry and not a git repository' + else: + error_msg += ': Update failed (check logs for details)' if api_v3.operation_history: api_v3.operation_history.record_operation( From d0d8b1982364d16687ec39f2a244f7209fe07359 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 16:12:20 -0500 Subject: [PATCH 13/20] fix(plugins): Add detailed logging for plugin update failures - Log git command that failed and return code - Add logging before/after update attempt - Log whether plugin is detected as git repository - Helps diagnose why updates fail for plugins installed from URLs --- src/plugin_system/store_manager.py | 4 +++- web_interface/blueprints/api_v3.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index b6bfd4fb9..14e288c37 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1616,7 +1616,9 @@ def update_plugin(self, plugin_id: str) -> bool: except subprocess.CalledProcessError as git_error: error_output = git_error.stderr or git_error.stdout or "Unknown error" - self.logger.warning(f"Git update failed for {plugin_id}: {error_output}") + self.logger.error(f"Git update failed for {plugin_id}: {error_output}") + self.logger.error(f"Git command that failed: {' '.join(git_error.cmd) if hasattr(git_error, 'cmd') else 'unknown'}") + self.logger.error(f"Return code: {git_error.returncode}") # Check if it's a merge conflict or local changes issue if "would be overwritten" in error_output or "local changes" in error_output.lower(): self.logger.warning(f"Plugin {plugin_id} has local changes that prevent update. Consider committing or stashing changes manually.") diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 06d61b092..8961c9b72 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -2179,12 +2179,23 @@ def update_plugin(): current_commit = git_info_before.get('sha') current_branch = git_info_before.get('branch') + # Check if plugin is a git repo first (for better error messages) + plugin_path_dir = Path(api_v3.plugin_store_manager.plugins_dir) / plugin_id + is_git_repo = False + if plugin_path_dir.exists(): + git_info = api_v3.plugin_store_manager._get_local_git_info(plugin_path_dir) + is_git_repo = git_info is not None + if is_git_repo: + print(f"[UPDATE] Plugin {plugin_id} is a git repository, will update via git pull") + remote_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id, fetch_latest_from_github=True) remote_commit = remote_info.get('last_commit_sha') if remote_info else None remote_branch = remote_info.get('branch') if remote_info else None # Update the plugin + print(f"[UPDATE] Attempting to update plugin {plugin_id}...") success = api_v3.plugin_store_manager.update_plugin(plugin_id) + print(f"[UPDATE] Update result for {plugin_id}: {success}") if success: updated_last_updated = current_last_updated From 2f3889ede04d4e9384523995ea59034ae635349d Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:22:57 -0500 Subject: [PATCH 14/20] fix(plugins): Add better logging for plugin update detection - Log when plugin is detected as git repository - Log when plugin is not a git repository - Provide helpful message for ZIP-installed plugins - Helps diagnose why updates fail for plugins installed from URLs --- src/plugin_system/store_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index 14e288c37..f5d3ae847 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1628,10 +1628,12 @@ def update_plugin(self, plugin_id: str) -> bool: return False # Not a git repository - try registry-based update + self.logger.info(f"Plugin {plugin_id} is not a git repository, checking registry...") self.fetch_registry(force_refresh=True) plugin_info_remote = self.get_plugin_info(plugin_id, fetch_latest_from_github=True) if not plugin_info_remote: self.logger.warning(f"Plugin {plugin_id} not found in registry and not a git repository; cannot update automatically") + self.logger.warning(f"Plugin may have been installed via ZIP download. Try reinstalling from GitHub URL to enable updates.") return False repo_url = plugin_info_remote.get('repo') From f1519c3328f28ef4bda0eb6a4ed0c9eb97b33597 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:23:17 -0500 Subject: [PATCH 15/20] fix(plugins): Enable updates for plugins installed from GitHub URLs - Get git remote URL from plugin directory even if .git is missing - If plugin not in registry but has remote URL, reinstall as git repo - Allows updating plugins installed from URLs even if git clone failed initially - Falls back to reinstalling from original URL to enable future updates --- src/plugin_system/store_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index f5d3ae847..38c58c713 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1338,6 +1338,16 @@ def _get_local_git_info(self, plugin_path: Path) -> Optional[Dict[str, str]]: if branch == 'HEAD': branch = '' + # Get remote URL + remote_url_result = subprocess.run( + ['git', '-C', str(plugin_path), 'config', '--get', 'remote.origin.url'], + capture_output=True, + text=True, + timeout=10, + check=False + ) + remote_url = remote_url_result.stdout.strip() if remote_url_result.returncode == 0 else None + # Get commit date in ISO format date_result = subprocess.run( ['git', '-C', str(plugin_path), 'log', '-1', '--format=%cI', 'HEAD'], @@ -1353,6 +1363,10 @@ def _get_local_git_info(self, plugin_path: Path) -> Optional[Dict[str, str]]: 'short_sha': sha[:7] if sha else '', 'branch': branch } + + # Add remote URL if available + if remote_url: + result['remote_url'] = remote_url # Add commit date if available if commit_date_iso: From a433aaca9baa2003abee5441127b73207ab7b232 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:23:31 -0500 Subject: [PATCH 16/20] fix(plugins): Reinstall from git remote URL if plugin not in registry - When plugin is not a git repo and not in registry, check for git remote URL - If remote URL exists, reinstall plugin from that URL to enable future updates - Handles case where plugin was installed from URL but git clone failed initially --- src/plugin_system/store_manager.py | 49 ++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index 38c58c713..1d97c01a6 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1641,13 +1641,58 @@ def update_plugin(self, plugin_id: str) -> bool: self.logger.warning(f"Git update timed out for {plugin_id}") return False - # Not a git repository - try registry-based update + # Not a git repository - try to get repo URL from git config if it exists + # (in case .git directory was removed but remote URL is still in config) + repo_url = None + try: + remote_url_result = subprocess.run( + ['git', '-C', str(plugin_path), 'config', '--get', 'remote.origin.url'], + capture_output=True, + text=True, + timeout=10, + check=False + ) + if remote_url_result.returncode == 0: + repo_url = remote_url_result.stdout.strip() + self.logger.info(f"Found git remote URL for {plugin_id}: {repo_url}") + except Exception as e: + self.logger.debug(f"Could not get git remote URL: {e}") + + # Try registry-based update self.logger.info(f"Plugin {plugin_id} is not a git repository, checking registry...") self.fetch_registry(force_refresh=True) plugin_info_remote = self.get_plugin_info(plugin_id, fetch_latest_from_github=True) + + # If not in registry but we have a repo URL, try reinstalling from that URL + if not plugin_info_remote and repo_url: + self.logger.info(f"Plugin {plugin_id} not in registry but has git remote URL. Reinstalling from {repo_url} to enable updates...") + try: + # Get current branch if possible + branch_result = subprocess.run( + ['git', '-C', str(plugin_path), 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, + text=True, + timeout=10, + check=False + ) + branch = branch_result.stdout.strip() if branch_result.returncode == 0 else None + if branch == 'HEAD' or not branch: + branch = 'main' + + # Reinstall from URL + result = self.install_from_url(repo_url, plugin_id=plugin_id, branch=branch) + if result.get('success'): + self.logger.info(f"Successfully reinstalled {plugin_id} from {repo_url} as git repository") + return True + else: + self.logger.warning(f"Failed to reinstall {plugin_id} from {repo_url}: {result.get('error')}") + except Exception as e: + self.logger.error(f"Error reinstalling {plugin_id} from URL: {e}") + if not plugin_info_remote: self.logger.warning(f"Plugin {plugin_id} not found in registry and not a git repository; cannot update automatically") - self.logger.warning(f"Plugin may have been installed via ZIP download. Try reinstalling from GitHub URL to enable updates.") + if not repo_url: + self.logger.warning(f"Plugin may have been installed via ZIP download. Try reinstalling from GitHub URL to enable updates.") return False repo_url = plugin_info_remote.get('repo') From c39182ac7771825d345c08b5c4b631fb46380a92 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:35:39 -0500 Subject: [PATCH 17/20] fix(plugins): Improve git update error handling and logging - Make git fetch non-fatal (log warning but continue) - Make git checkout non-fatal (log warning but continue) - Add detailed error messages for common git failures - Log which git command failed and return code - Better handling of authentication, merge conflicts, and unrelated histories --- src/plugin_system/store_manager.py | 37 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index 1d97c01a6..175fa1043 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1479,13 +1479,18 @@ def update_plugin(self, plugin_id: str) -> bool: self.logger.info(f"Updating {plugin_id} via git pull (local branch: {local_branch})...") try: # Fetch latest changes first to get all remote branch info - subprocess.run( + # If fetch fails, we'll still try to pull (might work with existing remote refs) + fetch_result = subprocess.run( ['git', '-C', str(plugin_path), 'fetch', 'origin'], capture_output=True, text=True, timeout=60, - check=True + check=False ) + if fetch_result.returncode != 0: + self.logger.warning(f"Git fetch failed for {plugin_id}: {fetch_result.stderr or fetch_result.stdout}. Will still attempt pull.") + else: + self.logger.debug(f"Successfully fetched remote changes for {plugin_id}") # Determine which remote branch to pull from # Strategy: Use what the local branch is tracking, or find the best match @@ -1560,13 +1565,15 @@ def update_plugin(self, plugin_id: str) -> bool: self.logger.info(f"Falling back to local branch name {local_branch} for pull") # Ensure we're on the local branch - subprocess.run( + checkout_result = subprocess.run( ['git', '-C', str(plugin_path), 'checkout', local_branch], capture_output=True, text=True, timeout=30, - check=True + check=False ) + if checkout_result.returncode != 0: + self.logger.warning(f"Git checkout to {local_branch} failed for {plugin_id}: {checkout_result.stderr or checkout_result.stdout}. Will still attempt pull.") # Check for local changes and stash them if needed # Use --untracked-files=no to skip untracked files check (much faster) @@ -1605,6 +1612,7 @@ def update_plugin(self, plugin_id: str) -> bool: self.logger.warning(f"Stash operation timed out for {plugin_id}, proceeding with pull") # Pull from the determined remote branch + self.logger.info(f"Pulling from origin/{remote_pull_branch} for {plugin_id}...") pull_result = subprocess.run( ['git', '-C', str(plugin_path), 'pull', 'origin', remote_pull_branch], capture_output=True, @@ -1630,12 +1638,25 @@ def update_plugin(self, plugin_id: str) -> bool: except subprocess.CalledProcessError as git_error: error_output = git_error.stderr or git_error.stdout or "Unknown error" - self.logger.error(f"Git update failed for {plugin_id}: {error_output}") - self.logger.error(f"Git command that failed: {' '.join(git_error.cmd) if hasattr(git_error, 'cmd') else 'unknown'}") + cmd_str = ' '.join(git_error.cmd) if hasattr(git_error, 'cmd') else 'unknown' + self.logger.error(f"Git update failed for {plugin_id}") + self.logger.error(f"Command: {cmd_str}") self.logger.error(f"Return code: {git_error.returncode}") - # Check if it's a merge conflict or local changes issue - if "would be overwritten" in error_output or "local changes" in error_output.lower(): + self.logger.error(f"Error output: {error_output}") + + # Check for specific error conditions + error_lower = error_output.lower() + if "would be overwritten" in error_output or "local changes" in error_lower: self.logger.warning(f"Plugin {plugin_id} has local changes that prevent update. Consider committing or stashing changes manually.") + elif "refusing to merge unrelated histories" in error_lower: + self.logger.error(f"Plugin {plugin_id} has unrelated git histories. Plugin may need to be reinstalled.") + elif "authentication" in error_lower or "permission denied" in error_lower: + self.logger.error(f"Authentication failed for {plugin_id}. Check git credentials or repository permissions.") + elif "not found" in error_lower or "does not exist" in error_lower: + self.logger.error(f"Remote branch or repository not found for {plugin_id}. Check repository URL and branch name.") + elif "merge conflict" in error_lower or "conflict" in error_lower: + self.logger.error(f"Merge conflict detected for {plugin_id}. Resolve conflicts manually or reinstall plugin.") + return False except subprocess.TimeoutExpired: self.logger.warning(f"Git update timed out for {plugin_id}") From 4c1fe7cdf2886e5c28aa5c6315f4ba7d0db64f13 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:42:51 -0500 Subject: [PATCH 18/20] fix(plugins): Add detailed exception logging to update endpoint - Log full traceback when update fails - Log exception details in catch block - Helps diagnose update failures from API endpoint --- web_interface/blueprints/api_v3.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 8961c9b72..bd0cbb283 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -2291,6 +2291,11 @@ def update_plugin(): error=error_msg ) + import traceback + error_details = traceback.format_exc() + print(f"[UPDATE] Update failed for {plugin_id}: {error_msg}") + print(f"[UPDATE] Traceback: {error_details}") + return error_response( ErrorCode.PLUGIN_UPDATE_FAILED, error_msg, @@ -2298,6 +2303,11 @@ def update_plugin(): ) except Exception as e: + import traceback + error_details = traceback.format_exc() + print(f"[UPDATE] Exception in update_plugin endpoint: {str(e)}") + print(f"[UPDATE] Traceback: {error_details}") + from src.web_interface.errors import WebInterfaceError error = WebInterfaceError.from_exception(e, ErrorCode.PLUGIN_UPDATE_FAILED) if api_v3.operation_history: From 805e70c50cdaadf9012dab980d79585e82dd22f9 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 20:45:24 -0500 Subject: [PATCH 19/20] fix(plugins): Handle untracked files during plugin update - Remove .dependencies_installed marker file before pull (safe to regenerate) - Stash untracked files using 'git stash -u' if they can't be removed - Prevents 'untracked files would be overwritten' errors during update - Fixes issue where .dependencies_installed blocks git pull --- src/plugin_system/store_manager.py | 45 +++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index 175fa1043..dc50cdc5d 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1575,9 +1575,39 @@ def update_plugin(self, plugin_id: str) -> bool: if checkout_result.returncode != 0: self.logger.warning(f"Git checkout to {local_branch} failed for {plugin_id}: {checkout_result.stderr or checkout_result.stdout}. Will still attempt pull.") - # Check for local changes and stash them if needed - # Use --untracked-files=no to skip untracked files check (much faster) + # Check for local changes and untracked files that might conflict + # First, check for untracked files that would be overwritten try: + # Check for untracked files + untracked_result = subprocess.run( + ['git', '-C', str(plugin_path), 'status', '--porcelain', '--untracked-files=all'], + capture_output=True, + text=True, + timeout=30, + check=False + ) + untracked_files = [] + if untracked_result.returncode == 0: + for line in untracked_result.stdout.strip().split('\n'): + if line.startswith('??'): + # Untracked file + file_path = line[3:].strip() + untracked_files.append(file_path) + + # Remove marker files that are safe to delete (they'll be regenerated) + safe_to_remove = ['.dependencies_installed'] + removed_files = [] + for file_name in safe_to_remove: + file_path = plugin_path / file_name + if file_path.exists() and file_name in untracked_files: + try: + file_path.unlink() + removed_files.append(file_name) + self.logger.info(f"Removed marker file {file_name} from {plugin_id} before update") + except Exception as e: + self.logger.warning(f"Could not remove {file_name} from {plugin_id}: {e}") + + # Check for tracked file changes status_result = subprocess.run( ['git', '-C', str(plugin_path), 'status', '--porcelain', '--untracked-files=no'], capture_output=True, @@ -1586,6 +1616,12 @@ def update_plugin(self, plugin_id: str) -> bool: check=False ) has_changes = bool(status_result.stdout.strip()) + + # If there are remaining untracked files (not safe to remove), stash them + remaining_untracked = [f for f in untracked_files if f not in removed_files] + if remaining_untracked: + self.logger.info(f"Found {len(remaining_untracked)} untracked files in {plugin_id}, will stash them") + has_changes = True except subprocess.TimeoutExpired: # If status check times out, assume there might be changes and proceed self.logger.warning(f"Git status check timed out for {plugin_id}, proceeding with update") @@ -1596,8 +1632,9 @@ def update_plugin(self, plugin_id: str) -> bool: if has_changes: self.logger.info(f"Stashing local changes in {plugin_id} before update") try: + # Use -u to include untracked files in stash stash_result = subprocess.run( - ['git', '-C', str(plugin_path), 'stash', 'push', '-m', f'LEDMatrix auto-stash before update {plugin_id}'], + ['git', '-C', str(plugin_path), 'stash', 'push', '-u', '-m', f'LEDMatrix auto-stash before update {plugin_id}'], capture_output=True, text=True, timeout=30, @@ -1605,7 +1642,7 @@ def update_plugin(self, plugin_id: str) -> bool: ) if stash_result.returncode == 0: stash_info = " (local changes were stashed)" - self.logger.info(f"Stashed local changes for {plugin_id}") + self.logger.info(f"Stashed local changes (including untracked files) for {plugin_id}") else: self.logger.warning(f"Failed to stash local changes for {plugin_id}: {stash_result.stderr}") except subprocess.TimeoutExpired: From 34c13582cc58e55b43862d658899181e36ada5ef Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 2 Jan 2026 22:18:02 -0500 Subject: [PATCH 20/20] chore: Update 7-segment-clock submodule with improved clone instructions --- plugins/7-segment-clock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/7-segment-clock b/plugins/7-segment-clock index 7e9c61003..61a9c71d6 160000 --- a/plugins/7-segment-clock +++ b/plugins/7-segment-clock @@ -1 +1 @@ -Subproject commit 7e9c610033a2fa75603e600743f43404f009b0d4 +Subproject commit 61a9c71d67cca4cd93a7fc1087c478207c59419c