diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index f9d647c..b9e663b 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/admin_api.py b/routes/admin_api.py index ddf7d9d..6b0bdf6 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -563,4 +563,31 @@ def generate_password_reset_token(current_user, user_id): 'reset_url': reset_url, 'expires_at': reset_token.expires_at.isoformat(), 'user_email': user.email - }) \ No newline at end of file + }) + +# Version Information +@admin_api.route('/version-info', methods=['GET']) +@csrf.exempt +@token_required +def get_version_info(current_user): + """Get version information from environment variables""" + try: + version_info = { + 'app_version': os.environ.get('APP_VERSION', 'unknown'), + 'git_commit': os.environ.get('GIT_COMMIT', 'unknown'), + 'git_branch': os.environ.get('GIT_BRANCH', 'unknown'), + 'deployed_at': os.environ.get('DEPLOYED_AT', 'unknown'), + 'ismaster': os.environ.get('ISMASTER', 'false'), + 'port': os.environ.get('PORT', 'unknown') + } + + return jsonify(version_info) + except Exception as e: + current_app.logger.error(f"Error getting version info: {str(e)}") + return jsonify({ + 'error': str(e), + 'app_version': 'unknown', + 'git_commit': 'unknown', + 'git_branch': 'unknown', + 'deployed_at': 'unknown' + }), 500 \ No newline at end of file diff --git a/routes/main.py b/routes/main.py index 37a3e4f..f862f00 100644 --- a/routes/main.py +++ b/routes/main.py @@ -625,6 +625,93 @@ def init_routes(main_bp): 'is_valid': is_valid }) + @main_bp.route('/instances//version-info') + @login_required + @require_password_change + def instance_version_info(instance_id): + if not os.environ.get('MASTER', 'false').lower() == 'true': + return jsonify({'error': 'Unauthorized'}), 403 + + instance = Instance.query.get_or_404(instance_id) + + # Check if instance has a connection token + if not instance.connection_token: + return jsonify({ + 'error': 'Instance not authenticated', + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + }) + + try: + # Get JWT token using the connection token + jwt_response = requests.post( + f"{instance.main_url.rstrip('/')}/api/admin/management-token", + headers={ + 'X-API-Key': instance.connection_token, + 'Accept': 'application/json' + }, + timeout=5 + ) + + if jwt_response.status_code != 200: + return jsonify({ + 'error': 'Failed to authenticate with instance', + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + }) + + jwt_data = jwt_response.json() + jwt_token = jwt_data.get('token') + + if not jwt_token: + return jsonify({ + 'error': 'No JWT token received', + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + }) + + # Fetch version information from the instance + response = requests.get( + f"{instance.main_url.rstrip('/')}/api/admin/version-info", + headers={ + 'Authorization': f'Bearer {jwt_token}', + 'Accept': 'application/json' + }, + timeout=5 + ) + + if response.status_code == 200: + version_data = response.json() + + # Update the instance with the fetched version information + instance.deployed_version = version_data.get('app_version', instance.deployed_version) + instance.deployed_branch = version_data.get('git_branch', instance.deployed_branch) + instance.version_checked_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'success': True, + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch, + 'git_commit': version_data.get('git_commit'), + 'deployed_at': version_data.get('deployed_at'), + 'version_checked_at': instance.version_checked_at.isoformat() if instance.version_checked_at else None + }) + else: + return jsonify({ + 'error': f'Failed to fetch version info: {response.status_code}', + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + }) + + except Exception as e: + current_app.logger.error(f"Error fetching version info: {str(e)}") + return jsonify({ + 'error': f'Error fetching version info: {str(e)}', + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + }) + UPLOAD_FOLDER = '/app/uploads/profile_pics' if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) diff --git a/templates/main/instances.html b/templates/main/instances.html index 76a7b47..9504a72 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -706,12 +706,20 @@ document.addEventListener('DOMContentLoaded', function() { const headerButtons = document.querySelector('.header-buttons'); if (headerButtons) { const refreshButton = document.createElement('button'); - refreshButton.className = 'btn btn-outline-primary'; - refreshButton.innerHTML = ' Refresh'; + refreshButton.className = 'btn btn-outline-primary me-2'; + refreshButton.innerHTML = ' Refresh All'; refreshButton.onclick = function() { fetchCompanyNames(); }; headerButtons.appendChild(refreshButton); + + const versionRefreshButton = document.createElement('button'); + versionRefreshButton.className = 'btn btn-outline-info'; + versionRefreshButton.innerHTML = ' Refresh Versions'; + versionRefreshButton.onclick = function() { + refreshAllVersionInfo(); + }; + headerButtons.appendChild(versionRefreshButton); } // Wait a short moment to ensure the table is rendered @@ -768,12 +776,25 @@ document.addEventListener('DOMContentLoaded', function() { updateColorPreview(); }); -// Function to check status of all instances +// Function to check all instance statuses async function checkAllInstanceStatuses() { - const statusBadges = document.querySelectorAll('[data-instance-id]'); - for (const badge of statusBadges) { + console.log('Checking all instance statuses...'); + const instances = document.querySelectorAll('[data-instance-id]'); + + for (const badge of instances) { const instanceId = badge.dataset.instanceId; await checkInstanceStatus(instanceId); + + // Also refresh version info when checking status + const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href; + const apiKey = badge.dataset.token; + + if (instanceUrl && apiKey) { + // Fetch version info in the background (don't await to avoid blocking status checks) + fetchVersionInfo(instanceUrl, instanceId).catch(error => { + console.error(`Error fetching version info for instance ${instanceId}:`, error); + }); + } } } @@ -890,6 +911,115 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) { } } +// Function to fetch version information for an instance +async function fetchVersionInfo(instanceUrl, instanceId) { + const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr'); + const versionCell = row.querySelector('td:nth-child(9)'); // Version column + const branchCell = row.querySelector('td:nth-child(10)'); // Branch column + + // Show loading state + if (versionCell) { + versionCell.innerHTML = ' Loading...'; + } + if (branchCell) { + branchCell.innerHTML = ' Loading...'; + } + + try { + const apiKey = document.querySelector(`[data-instance-id="${instanceId}"]`).dataset.token; + if (!apiKey) { + throw new Error('No API key available'); + } + + console.log(`Getting JWT token for instance ${instanceId} for version info`); + const jwtToken = await getJWTToken(instanceUrl, apiKey); + console.log('Got JWT token for version info'); + + // Fetch version information + console.log(`Fetching version info for instance ${instanceId} from ${instanceUrl}/api/admin/version-info`); + const response = await fetch(`${instanceUrl}/api/admin/version-info`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${jwtToken}` + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`HTTP error ${response.status}:`, errorText); + throw new Error(`Server returned ${response.status}: ${errorText}`); + } + + const data = await response.json(); + console.log('Received version data:', data); + + // Update version cell + if (versionCell) { + const appVersion = data.app_version || 'unknown'; + const gitCommit = data.git_commit || 'unknown'; + const deployedAt = data.deployed_at || 'unknown'; + + if (appVersion !== 'unknown') { + versionCell.innerHTML = ` + + ${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion} + `; + } else { + versionCell.innerHTML = 'unknown'; + } + } + + // Update branch cell + if (branchCell) { + const gitBranch = data.git_branch || 'unknown'; + + if (gitBranch !== 'unknown') { + branchCell.innerHTML = ` + + ${gitBranch} + `; + } else { + branchCell.innerHTML = 'unknown'; + } + } + + // Update tooltips + const versionBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]'); + const branchBadge = branchCell?.querySelector('[data-bs-toggle="tooltip"]'); + + if (versionBadge) { + new bootstrap.Tooltip(versionBadge); + } + if (branchBadge) { + new bootstrap.Tooltip(branchBadge); + } + + } catch (error) { + console.error(`Error fetching version info for instance ${instanceId}:`, error); + + // Show error state + if (versionCell) { + versionCell.innerHTML = ` + + Error + `; + } + if (branchCell) { + branchCell.innerHTML = ` + + Error + `; + } + + // Add tooltips for error states + const errorBadges = [versionCell, branchCell].map(cell => cell?.querySelector('[data-bs-toggle="tooltip"]')).filter(Boolean); + errorBadges.forEach(badge => new bootstrap.Tooltip(badge)); + } +} + // Function to fetch company name from instance settings async function fetchCompanyName(instanceUrl, instanceId) { const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr'); @@ -977,53 +1107,31 @@ async function fetchCompanyName(instanceUrl, instanceId) { // Function to fetch company names for all instances async function fetchCompanyNames() { + console.log('Starting fetchCompanyNames...'); + const instances = document.querySelectorAll('[data-instance-id]'); const loadingPromises = []; - console.log('Starting to fetch company names and stats for all instances'); - - for (const instance of instances) { - const instanceId = instance.dataset.instanceId; - const row = instance.closest('tr'); + for (const badge of instances) { + const instanceId = badge.dataset.instanceId; + const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href; + const apiKey = badge.dataset.token; - // Debug: Log all cells in the row - console.log(`Row for instance ${instanceId}:`, { - cells: Array.from(row.querySelectorAll('td')).map(td => ({ - text: td.textContent.trim(), - html: td.innerHTML.trim() - })) - }); - - // Main URL is now the 9th column (after adding Version and Branch columns) - const urlCell = row.querySelector('td:nth-child(9)'); - - if (!urlCell) { - console.error(`Could not find URL cell for instance ${instanceId}`); - continue; - } - - const urlLink = urlCell.querySelector('a'); - if (!urlLink) { - console.error(`Could not find URL link for instance ${instanceId}`); - continue; - } - - const instanceUrl = urlLink.getAttribute('href'); - const token = instance.dataset.token; - - console.log(`Instance ${instanceId}:`, { - url: instanceUrl, - hasToken: !!token - }); - - if (instanceUrl && token) { - loadingPromises.push(fetchCompanyName(instanceUrl, instanceId)); + if (instanceUrl && apiKey) { + console.log(`Fetching data for instance ${instanceId}`); + loadingPromises.push( + fetchCompanyName(instanceUrl, instanceId), + fetchVersionInfo(instanceUrl, instanceId) // Add version info fetching + ); } else { + const row = badge.closest('tr'); const cells = [ row.querySelector('td:nth-child(2)'), // Company row.querySelector('td:nth-child(3)'), // Rooms row.querySelector('td:nth-child(4)'), // Conversations - row.querySelector('td:nth-child(5)') // Data + row.querySelector('td:nth-child(5)'), // Data + row.querySelector('td:nth-child(9)'), // Version + row.querySelector('td:nth-child(10)') // Branch ]; cells.forEach(cell => { @@ -1043,7 +1151,7 @@ async function fetchCompanyNames() { try { await Promise.all(loadingPromises); - console.log('Finished fetching all company names and stats'); + console.log('Finished fetching all company names, stats, and version info'); } catch (error) { console.error('Error in fetchCompanyNames:', error); } @@ -1886,5 +1994,30 @@ function launchInstance() { // Redirect to the launch progress page window.location.href = '/instances/launch-progress'; } + +// Function to refresh all version information +async function refreshAllVersionInfo() { + console.log('Refreshing all version information...'); + const instances = document.querySelectorAll('[data-instance-id]'); + const loadingPromises = []; + + for (const badge of instances) { + const instanceId = badge.dataset.instanceId; + const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href; + const apiKey = badge.dataset.token; + + if (instanceUrl && apiKey) { + console.log(`Refreshing version info for instance ${instanceId}`); + loadingPromises.push(fetchVersionInfo(instanceUrl, instanceId)); + } + } + + try { + await Promise.all(loadingPromises); + console.log('Finished refreshing all version information'); + } catch (error) { + console.error('Error refreshing version information:', error); + } +} {% endblock %} \ No newline at end of file