diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index b9e663b..63a275d 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ 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