From 4cf9cca11692a46a070675616013fe1a83eb33fe Mon Sep 17 00:00:00 2001 From: Kobe Date: Mon, 23 Jun 2025 15:46:29 +0200 Subject: [PATCH] version display on instances page --- routes/__pycache__/main.cpython-313.pyc | Bin 105155 -> 109093 bytes routes/main.py | 95 ++++++++++ templates/main/instances.html | 242 ++++++++++++++++++------ 3 files changed, 276 insertions(+), 61 deletions(-) diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index b9e663b4b1799cb9f99230f3342818e6cc65a6cd..63a275d5e70761b79738b1bcbeaee83c0fcabf8e 100644 GIT binary patch delta 7505 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 A$p8QV diff --git a/routes/main.py b/routes/main.py index f862f00..9c5599d 100644 --- a/routes/main.py +++ b/routes/main.py @@ -712,6 +712,101 @@ def init_routes(main_bp): 'deployed_branch': instance.deployed_branch }) + @main_bp.route('/api/latest-version') + @login_required + @require_password_change + def get_latest_version(): + if not os.environ.get('MASTER', 'false').lower() == 'true': + return jsonify({'error': 'Unauthorized'}), 403 + + try: + # Get Git settings + git_settings = KeyValueSettings.get_value('git_settings') + if not git_settings: + return jsonify({ + 'error': 'Git settings not configured', + 'latest_version': 'unknown', + 'latest_commit': 'unknown', + 'last_checked': None + }) + + latest_tag = None + latest_commit = None + + if git_settings['provider'] == 'gitea': + headers = { + 'Accept': 'application/json', + 'Authorization': f'token {git_settings["token"]}' + } + + # Get the latest tag + tags_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{git_settings["repo"]}/tags', + headers=headers, + timeout=10 + ) + + if tags_response.status_code == 200: + tags_data = tags_response.json() + if tags_data: + # Sort tags by commit date (newest first) and get the latest + sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True) + if sorted_tags: + latest_tag = sorted_tags[0].get('name') + latest_commit = sorted_tags[0].get('commit', {}).get('id') + else: + # Try token as query parameter if header auth fails + tags_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{git_settings["repo"]}/tags?token={git_settings["token"]}', + headers={'Accept': 'application/json'}, + timeout=10 + ) + if tags_response.status_code == 200: + tags_data = tags_response.json() + if tags_data: + sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True) + if sorted_tags: + latest_tag = sorted_tags[0].get('name') + latest_commit = sorted_tags[0].get('commit', {}).get('id') + + elif git_settings['provider'] == 'gitlab': + headers = { + 'PRIVATE-TOKEN': git_settings['token'], + 'Accept': 'application/json' + } + + # Get the latest tag + tags_response = requests.get( + f'{git_settings["url"]}/api/v4/projects/{git_settings["repo"].replace("/", "%2F")}/repository/tags', + headers=headers, + params={'order_by': 'version', 'sort': 'desc', 'per_page': 1}, + timeout=10 + ) + + if tags_response.status_code == 200: + tags_data = tags_response.json() + if tags_data: + latest_tag = tags_data[0].get('name') + latest_commit = tags_data[0].get('commit', {}).get('id') + + return jsonify({ + 'success': True, + 'latest_version': latest_tag or 'unknown', + 'latest_commit': latest_commit or 'unknown', + 'repository': git_settings.get('repo', 'unknown'), + 'provider': git_settings.get('provider', 'unknown'), + 'last_checked': datetime.utcnow().isoformat() + }) + + except Exception as e: + current_app.logger.error(f"Error fetching latest version: {str(e)}") + return jsonify({ + 'error': f'Error fetching latest version: {str(e)}', + 'latest_version': 'unknown', + 'latest_commit': 'unknown', + 'last_checked': datetime.utcnow().isoformat() + }), 500 + 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 9504a72..61ec53b 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -45,27 +45,57 @@ {% block content %} {{ header( - title="Instances", - description="Manage your DocuPulse instances", + title="Instance Management", + description="Manage and monitor your DocuPulse instances", icon="fa-server", buttons=[ { 'text': 'Launch New Instance', - 'url': '#', - 'icon': 'fa-rocket', - 'class': 'btn-primary', - 'onclick': 'showAddInstanceModal()' - }, - { - 'text': 'Add Existing Instance', - 'url': '#', - 'icon': 'fa-link', - 'class': 'btn-primary', - 'onclick': 'showAddExistingInstanceModal()' + 'onclick': 'showAddInstanceModal()', + 'icon': 'fa-plus', + 'class': 'btn-primary' } ] ) }} + +
+
+
+
+
+
+
+
+ + Latest Available Version +
+
+
+ + Loading... + +
+
+ + + Last checked: Loading... + +
+
+
+
+ +
+
+
+
+
+
+
+
@@ -84,7 +114,6 @@ Main URL Status Version - Branch Connection Token Actions @@ -126,16 +155,6 @@ unknown {% endif %} - - {% if instance.deployed_branch %} - - {{ instance.deployed_branch }} - - {% else %} - unknown - {% endif %} - {% if instance.connection_token %} @@ -729,10 +748,16 @@ document.addEventListener('DOMContentLoaded', function() { // Fetch company names for all instances fetchCompanyNames(); + + // Fetch latest version information + fetchLatestVersion(); }, 100); // Set up periodic status checks (every 30 seconds) setInterval(checkAllInstanceStatuses, 30000); + + // Set up periodic latest version checks (every 5 minutes) + setInterval(fetchLatestVersion, 300000); // Update color picker functionality const primaryColor = document.getElementById('primaryColor'); @@ -914,16 +939,12 @@ 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 + const versionCell = row.querySelector('td:nth-child(9)'); // Version column (adjusted after removing branch) // Show loading state if (versionCell) { versionCell.innerHTML = ' Loading...'; } - if (branchCell) { - branchCell.innerHTML = ' Loading...'; - } try { const apiKey = document.querySelector(`[data-instance-id="${instanceId}"]`).dataset.token; @@ -961,41 +982,41 @@ async function fetchVersionInfo(instanceUrl, instanceId) { const deployedAt = data.deployed_at || 'unknown'; if (appVersion !== 'unknown') { + // Get the latest version for comparison + const latestVersionBadge = document.getElementById('latestVersionBadge'); + const latestVersion = latestVersionBadge ? latestVersionBadge.textContent.replace('Loading...', '').trim() : null; + + // Determine if this instance is up to date + let badgeClass = 'bg-secondary'; + let statusIcon = 'fas fa-tag'; + let tooltipText = `App Version: ${appVersion}
Git Commit: ${gitCommit}
Deployed: ${deployedAt}`; + + if (latestVersion && appVersion === latestVersion) { + badgeClass = 'bg-success'; + statusIcon = 'fas fa-check-circle'; + tooltipText += '
✅ Up to date'; + } else if (latestVersion && appVersion !== latestVersion) { + badgeClass = 'bg-danger'; + statusIcon = 'fas fa-exclamation-triangle'; + tooltipText += `
⚠️ Outdated (Latest: ${latestVersion})`; + } + versionCell.innerHTML = ` - - ${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion} + + ${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); @@ -1007,16 +1028,12 @@ async function fetchVersionInfo(instanceUrl, instanceId) { 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)); + const errorBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]'); + if (errorBadge) { + new bootstrap.Tooltip(errorBadge); + } } } @@ -1130,8 +1147,7 @@ async function fetchCompanyNames() { 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(9)'), // Version - row.querySelector('td:nth-child(10)') // Branch + row.querySelector('td:nth-child(9)') // Version ]; cells.forEach(cell => { @@ -2019,5 +2035,109 @@ async function refreshAllVersionInfo() { console.error('Error refreshing version information:', error); } } + +// Function to fetch latest version information +async function fetchLatestVersion() { + console.log('Fetching latest version information...'); + + const versionBadge = document.getElementById('latestVersionBadge'); + const commitSpan = document.getElementById('latestCommit'); + const checkedSpan = document.getElementById('lastChecked'); + + // Show loading state + if (versionBadge) { + versionBadge.innerHTML = ' Loading...'; + versionBadge.className = 'badge bg-secondary fs-6'; + } + + try { + const response = await fetch('/api/latest-version', { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + } + }); + + 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 latest version data:', data); + + if (data.success) { + // Update version badge + if (versionBadge) { + const version = data.latest_version; + if (version !== 'unknown') { + versionBadge.innerHTML = `${version}`; + versionBadge.className = 'badge bg-success fs-6'; + } else { + versionBadge.innerHTML = 'Unknown'; + versionBadge.className = 'badge bg-warning fs-6'; + } + } + + // Update commit information + if (commitSpan) { + const commit = data.latest_commit; + if (commit !== 'unknown') { + commitSpan.textContent = commit.substring(0, 8); + commitSpan.title = commit; + } else { + commitSpan.textContent = 'Unknown'; + } + } + + // Update last checked time + if (checkedSpan) { + const lastChecked = data.last_checked; + if (lastChecked) { + const date = new Date(lastChecked); + checkedSpan.textContent = date.toLocaleString(); + } else { + checkedSpan.textContent = 'Never'; + } + } + } else { + // Handle error response + if (versionBadge) { + versionBadge.innerHTML = 'Error'; + versionBadge.className = 'badge bg-danger fs-6'; + } + if (commitSpan) { + commitSpan.textContent = 'Error'; + } + if (checkedSpan) { + checkedSpan.textContent = 'Error'; + } + console.error('Error in latest version response:', data.error); + } + + } catch (error) { + console.error('Error fetching latest version:', error); + + // Show error state + if (versionBadge) { + versionBadge.innerHTML = 'Error'; + versionBadge.className = 'badge bg-danger fs-6'; + } + if (commitSpan) { + commitSpan.textContent = 'Error'; + } + if (checkedSpan) { + checkedSpan.textContent = 'Error'; + } + } +} + +// Function to refresh latest version (called by button) +async function refreshLatestVersion() { + console.log('Manual refresh of latest version requested'); + await fetchLatestVersion(); +} {% endblock %} \ No newline at end of file