fix(plugins): Fix GitHub install and update functionality for plugins installed from URLs (#167)

* 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.

* 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

* 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

* 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

* 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

* fix(plugins): Add fallback handler attachment after page load

* 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

* fix(plugins): Add more prominent logging markers for easier debugging

* 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

* chore: Update 7-segment-clock plugin submodule

- Update to latest version with scaling support
- Includes compatible_versions field fix for plugin store installation

* 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

* 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

* 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

* 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

* 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

* 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

* 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

* 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

* 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

* chore: Update 7-segment-clock submodule with improved clone instructions

---------

Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
Chuck
2026-01-03 09:44:51 -05:00
committed by GitHub
parent 9f1711f9a3
commit a13bd971b3
6 changed files with 513 additions and 128 deletions

3
.gitmodules vendored
View File

@@ -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

View File

@@ -1338,6 +1338,16 @@ class PluginStoreManager:
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 @@ class PluginStoreManager:
'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:
@@ -1465,13 +1479,18 @@ class PluginStoreManager:
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
@@ -1546,17 +1565,49 @@ class PluginStoreManager:
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)
# 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,
@@ -1565,6 +1616,12 @@ class PluginStoreManager:
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")
@@ -1575,8 +1632,9 @@ class PluginStoreManager:
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,
@@ -1584,13 +1642,14 @@ class PluginStoreManager:
)
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:
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,
@@ -1616,20 +1675,82 @@ class PluginStoreManager:
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}")
# 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():
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}")
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}")
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")
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')

View File

@@ -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
@@ -2259,9 +2270,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(
@@ -2271,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,
@@ -2278,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:

View File

@@ -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);
}
}
}
};
@@ -548,6 +560,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 = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a GitHub URL</span>';
}
return;
}
if (!repoUrl.includes('github.com')) {
if (statusDiv) {
statusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
}
return;
}
// Disable button and show loading
if (installBtn) {
installBtn.disabled = true;
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Installing...';
}
if (statusDiv) {
statusDiv.innerHTML = '<span class="text-blue-600"><i class="fas fa-spinner fa-spin mr-1"></i>Installing plugin...</span>';
}
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 = `<span class="text-green-600"><i class="fas fa-check-circle mr-1"></i>Successfully installed: ${data.plugin_id}</span>`;
}
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 = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>${data.message || 'Installation failed'}</span>`;
}
if (typeof showNotification === 'function') {
showNotification(data.message || 'Installation failed', 'error');
}
}
})
.catch(error => {
console.error('[handleGitHubPluginInstall] Error:', error);
if (statusDiv) {
statusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>Error: ${error.message}</span>`;
}
if (typeof showNotification === 'function') {
showNotification('Error installing plugin: ' + error.message, 'error');
}
})
.finally(() => {
if (installBtn) {
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-download mr-2"></i>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
@@ -935,11 +1057,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 +1109,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();
@@ -4474,6 +4609,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...');
@@ -4571,8 +4748,136 @@ 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');
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('[attachInstallButtonHandler] Install button cloned and replaced, type:', newBtn.type);
newBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('[attachInstallButtonHandler] Install button clicked!');
const repoUrl = pluginUrlInput.value.trim();
if (!repoUrl) {
if (pluginStatusDiv) {
pluginStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a GitHub URL</span>';
}
return;
}
if (!repoUrl.includes('github.com')) {
if (pluginStatusDiv) {
pluginStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
}
return;
}
newBtn.disabled = true;
newBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Installing...';
if (pluginStatusDiv) {
pluginStatusDiv.innerHTML = '<span class="text-blue-600"><i class="fas fa-spinner fa-spin mr-1"></i>Installing plugin...</span>';
}
const branch = document.getElementById('plugin-branch-input')?.value?.trim() || null;
const requestBody = { repo_url: repoUrl };
if (branch) {
requestBody.branch = branch;
}
console.log('[attachInstallButtonHandler] Sending install request:', requestBody);
fetch('/api/v3/plugins/install-from-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.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 = `<span class="text-green-600"><i class="fas fa-check-circle mr-1"></i>Successfully installed: ${data.plugin_id}</span>`;
}
pluginUrlInput.value = '';
// Refresh installed plugins list
setTimeout(() => {
loadInstalledPlugins();
}, 1000);
} else {
if (pluginStatusDiv) {
pluginStatusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>${data.message || 'Installation failed'}</span>`;
}
}
})
.catch(error => {
console.error('[attachInstallButtonHandler] Error:', error);
if (pluginStatusDiv) {
pluginStatusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>Error: ${error.message}</span>`;
}
})
.finally(() => {
newBtn.disabled = false;
newBtn.innerHTML = '<i class="fas fa-download mr-2"></i>Install';
});
});
// Allow Enter key to trigger install
pluginUrlInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
console.log('[attachInstallButtonHandler] Enter key pressed, triggering install');
newBtn.click();
}
});
console.log('[attachInstallButtonHandler] Install button handler attached successfully');
} else {
console.error('[attachInstallButtonHandler] Install button parent not found!');
}
} else {
console.warn('[attachInstallButtonHandler] Install button or URL input not found:', {
installBtn: !!installBtn,
pluginUrlInput: !!pluginUrlInput
});
}
}
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');
@@ -4616,6 +4921,13 @@ function setupGitHubInstallHandlers() {
}
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)
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 {
// Hide section - add hidden, set display none
section.classList.add('hidden');
@@ -4634,71 +4946,10 @@ function setupGitHubInstallHandlers() {
console.warn('[setupGitHubInstallHandlers] Required elements not found');
}
// Install single plugin from URL
const installBtn = document.getElementById('install-plugin-from-url');
const pluginUrlInput = document.getElementById('github-plugin-url');
const pluginStatusDiv = document.getElementById('github-plugin-status');
if (installBtn && pluginUrlInput) {
installBtn.addEventListener('click', function() {
const repoUrl = pluginUrlInput.value.trim();
if (!repoUrl) {
pluginStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a GitHub URL</span>';
return;
}
if (!repoUrl.includes('github.com')) {
pluginStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
return;
}
installBtn.disabled = true;
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Installing...';
pluginStatusDiv.innerHTML = '<span class="text-blue-600"><i class="fas fa-spinner fa-spin mr-1"></i>Installing plugin...</span>';
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') {
pluginStatusDiv.innerHTML = `<span class="text-green-600"><i class="fas fa-check-circle mr-1"></i>Successfully installed: ${data.plugin_id}</span>`;
pluginUrlInput.value = '';
// Refresh installed plugins list
setTimeout(() => {
loadInstalledPlugins();
}, 1000);
} else {
pluginStatusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>${data.message || 'Installation failed'}</span>`;
}
})
.catch(error => {
pluginStatusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>Error: ${error.message}</span>`;
})
.finally(() => {
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-download mr-2"></i>Install';
});
});
// Allow Enter key to trigger install
pluginUrlInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
installBtn.click();
}
});
}
// 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');
@@ -4878,48 +5129,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') {
@@ -6081,6 +6290,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
@@ -6125,5 +6343,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);

View File

@@ -221,7 +221,9 @@
<input type="text" id="github-plugin-url"
placeholder="https://github.com/user/ledmatrix-plugin-name"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
<button id="install-plugin-from-url" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md whitespace-nowrap">
<button type="button" id="install-plugin-from-url"
onclick="if(window.handleGitHubPluginInstall){window.handleGitHubPluginInstall()}else{alert('Function not loaded yet, please refresh the page')}"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md whitespace-nowrap">
<i class="fas fa-download mr-2"></i>Install
</button>
</div>