diff --git a/.gitmodules b/.gitmodules
index c911c51b..24143855 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 00000000..61a9c71d
--- /dev/null
+++ b/plugins/7-segment-clock
@@ -0,0 +1 @@
+Subproject commit 61a9c71d67cca4cd93a7fc1087c478207c59419c
diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py
index b6bfd4fb..dc50cdc5 100644
--- a/src/plugin_system/store_manager.py
+++ b/src/plugin_system/store_manager.py
@@ -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')
diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py
index 281bd4ff..bd0cbb28 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
@@ -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:
diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js
index 3aae8300..353714e8 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);
+ }
}
}
};
@@ -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 = '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
@@ -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 = '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;
+ }
+
+ 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 = `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 => {
+ console.error('[attachInstallButtonHandler] Error:', error);
+ if (pluginStatusDiv) {
+ pluginStatusDiv.innerHTML = `Error: ${error.message}`;
+ }
+ })
+ .finally(() => {
+ newBtn.disabled = false;
+ newBtn.innerHTML = '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 = '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...';
-
- 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 = `Successfully installed: ${data.plugin_id}`;
- pluginUrlInput.value = '';
-
- // Refresh installed plugins list
- setTimeout(() => {
- loadInstalledPlugins();
- }, 1000);
- } else {
- pluginStatusDiv.innerHTML = `${data.message || 'Installation failed'}`;
- }
- })
- .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();
- }
- });
- }
+ // 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);
diff --git a/web_interface/templates/v3/partials/plugins.html b/web_interface/templates/v3/partials/plugins.html
index e6ea736b..c77eb9e4 100644
--- a/web_interface/templates/v3/partials/plugins.html
+++ b/web_interface/templates/v3/partials/plugins.html
@@ -221,7 +221,9 @@
-