version display on instances page

This commit is contained in:
2025-06-23 15:46:29 +02:00
parent af375a2b5c
commit 4cf9cca116
3 changed files with 276 additions and 61 deletions

View File

@@ -712,6 +712,101 @@ def init_routes(main_bp):
'deployed_branch': instance.deployed_branch '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' UPLOAD_FOLDER = '/app/uploads/profile_pics'
if not os.path.exists(UPLOAD_FOLDER): if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER) os.makedirs(UPLOAD_FOLDER)

View File

@@ -45,27 +45,57 @@
{% block content %} {% block content %}
{{ header( {{ header(
title="Instances", title="Instance Management",
description="Manage your DocuPulse instances", description="Manage and monitor your DocuPulse instances",
icon="fa-server", icon="fa-server",
buttons=[ buttons=[
{ {
'text': 'Launch New Instance', 'text': 'Launch New Instance',
'url': '#', 'onclick': 'showAddInstanceModal()',
'icon': 'fa-rocket', 'icon': 'fa-plus',
'class': 'btn-primary', 'class': 'btn-primary'
'onclick': 'showAddInstanceModal()'
},
{
'text': 'Add Existing Instance',
'url': '#',
'icon': 'fa-link',
'class': 'btn-primary',
'onclick': 'showAddExistingInstanceModal()'
} }
] ]
) }} ) }}
<!-- Latest Version Information -->
<div class="container-fluid mb-4">
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="card-title mb-2">
<i class="fas fa-code-branch me-2" style="color: var(--primary-color);"></i>
Latest Available Version
</h5>
<div class="d-flex align-items-center">
<div class="me-4">
<span class="badge bg-success fs-6" id="latestVersionBadge">
<i class="fas fa-spinner fa-spin me-1"></i> Loading...
</span>
</div>
<div>
<small class="text-muted">
<i class="fas fa-clock me-1"></i>
Last checked: <span id="lastChecked" class="text-muted">Loading...</span>
</small>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-outline-primary btn-sm" onclick="refreshLatestVersion()">
<i class="fas fa-sync-alt me-1"></i> Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@@ -84,7 +114,6 @@
<th>Main URL</th> <th>Main URL</th>
<th>Status</th> <th>Status</th>
<th>Version</th> <th>Version</th>
<th>Branch</th>
<th>Connection Token</th> <th>Connection Token</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@@ -126,16 +155,6 @@
<span class="badge bg-secondary version-badge">unknown</span> <span class="badge bg-secondary version-badge">unknown</span>
{% endif %} {% endif %}
</td> </td>
<td>
{% if instance.deployed_branch %}
<span class="badge bg-light text-dark branch-badge" data-bs-toggle="tooltip"
title="Deployed branch: {{ instance.deployed_branch }}">
{{ instance.deployed_branch }}
</span>
{% else %}
<span class="badge bg-secondary branch-badge">unknown</span>
{% endif %}
</td>
<td> <td>
{% if instance.connection_token %} {% if instance.connection_token %}
<span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated"> <span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated">
@@ -729,11 +748,17 @@ document.addEventListener('DOMContentLoaded', function() {
// Fetch company names for all instances // Fetch company names for all instances
fetchCompanyNames(); fetchCompanyNames();
// Fetch latest version information
fetchLatestVersion();
}, 100); }, 100);
// Set up periodic status checks (every 30 seconds) // Set up periodic status checks (every 30 seconds)
setInterval(checkAllInstanceStatuses, 30000); setInterval(checkAllInstanceStatuses, 30000);
// Set up periodic latest version checks (every 5 minutes)
setInterval(fetchLatestVersion, 300000);
// Update color picker functionality // Update color picker functionality
const primaryColor = document.getElementById('primaryColor'); const primaryColor = document.getElementById('primaryColor');
const secondaryColor = document.getElementById('secondaryColor'); const secondaryColor = document.getElementById('secondaryColor');
@@ -914,16 +939,12 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) {
// Function to fetch version information for an instance // Function to fetch version information for an instance
async function fetchVersionInfo(instanceUrl, instanceId) { async function fetchVersionInfo(instanceUrl, instanceId) {
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr'); const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr');
const versionCell = row.querySelector('td:nth-child(9)'); // Version column const versionCell = row.querySelector('td:nth-child(9)'); // Version column (adjusted after removing branch)
const branchCell = row.querySelector('td:nth-child(10)'); // Branch column
// Show loading state // Show loading state
if (versionCell) { if (versionCell) {
versionCell.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...'; versionCell.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
} }
if (branchCell) {
branchCell.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
}
try { try {
const apiKey = document.querySelector(`[data-instance-id="${instanceId}"]`).dataset.token; 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'; const deployedAt = data.deployed_at || 'unknown';
if (appVersion !== '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}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}`;
if (latestVersion && appVersion === latestVersion) {
badgeClass = 'bg-success';
statusIcon = 'fas fa-check-circle';
tooltipText += '<br><strong>✅ Up to date</strong>';
} else if (latestVersion && appVersion !== latestVersion) {
badgeClass = 'bg-danger';
statusIcon = 'fas fa-exclamation-triangle';
tooltipText += `<br><strong>⚠️ Outdated (Latest: ${latestVersion})</strong>`;
}
versionCell.innerHTML = ` versionCell.innerHTML = `
<span class="badge bg-info version-badge" data-bs-toggle="tooltip" <span class="badge ${badgeClass} version-badge" data-bs-toggle="tooltip"
title="App Version: ${appVersion}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}"> title="${tooltipText}">
${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion} <i class="${statusIcon} me-1"></i>${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
</span>`; </span>`;
} else { } else {
versionCell.innerHTML = '<span class="badge bg-secondary version-badge">unknown</span>'; versionCell.innerHTML = '<span class="badge bg-secondary version-badge">unknown</span>';
} }
} }
// Update branch cell
if (branchCell) {
const gitBranch = data.git_branch || 'unknown';
if (gitBranch !== 'unknown') {
branchCell.innerHTML = `
<span class="badge bg-light text-dark branch-badge" data-bs-toggle="tooltip"
title="Deployed branch: ${gitBranch}">
${gitBranch}
</span>`;
} else {
branchCell.innerHTML = '<span class="badge bg-secondary branch-badge">unknown</span>';
}
}
// Update tooltips // Update tooltips
const versionBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]'); const versionBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
const branchBadge = branchCell?.querySelector('[data-bs-toggle="tooltip"]');
if (versionBadge) { if (versionBadge) {
new bootstrap.Tooltip(versionBadge); new bootstrap.Tooltip(versionBadge);
} }
if (branchBadge) {
new bootstrap.Tooltip(branchBadge);
}
} catch (error) { } catch (error) {
console.error(`Error fetching version info for instance ${instanceId}:`, error); console.error(`Error fetching version info for instance ${instanceId}:`, error);
@@ -1007,16 +1028,12 @@ async function fetchVersionInfo(instanceUrl, instanceId) {
<i class="fas fa-exclamation-triangle"></i> Error <i class="fas fa-exclamation-triangle"></i> Error
</span>`; </span>`;
} }
if (branchCell) {
branchCell.innerHTML = `
<span class="text-warning" data-bs-toggle="tooltip" title="Error: ${error.message}">
<i class="fas fa-exclamation-triangle"></i> Error
</span>`;
}
// Add tooltips for error states // Add tooltips for error states
const errorBadges = [versionCell, branchCell].map(cell => cell?.querySelector('[data-bs-toggle="tooltip"]')).filter(Boolean); const errorBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
errorBadges.forEach(badge => new bootstrap.Tooltip(badge)); 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(3)'), // Rooms
row.querySelector('td:nth-child(4)'), // Conversations 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(9)') // Version
row.querySelector('td:nth-child(10)') // Branch
]; ];
cells.forEach(cell => { cells.forEach(cell => {
@@ -2019,5 +2035,109 @@ async function refreshAllVersionInfo() {
console.error('Error refreshing version information:', error); 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 = '<i class="fas fa-spinner fa-spin me-1"></i> 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 = `<i class="fas fa-tag me-1"></i>${version}`;
versionBadge.className = 'badge bg-success fs-6';
} else {
versionBadge.innerHTML = '<i class="fas fa-question-circle me-1"></i>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 = '<i class="fas fa-exclamation-triangle me-1"></i>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 = '<i class="fas fa-exclamation-triangle me-1"></i>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();
}
</script> </script>
{% endblock %} {% endblock %}