mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
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:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
1
plugins/7-segment-clock
Submodule
1
plugins/7-segment-clock
Submodule
Submodule plugins/7-segment-clock added at 61a9c71d67
@@ -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'],
|
||||
@@ -1354,6 +1364,10 @@ class PluginStoreManager:
|
||||
'branch': branch
|
||||
}
|
||||
|
||||
# Add remote URL if available
|
||||
if remote_url:
|
||||
result['remote_url'] = remote_url
|
||||
|
||||
# Add commit date if available
|
||||
if commit_date_iso:
|
||||
result['date_iso'] = 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')
|
||||
|
||||
@@ -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:
|
||||
# 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'
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user