Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5168c27bf | |||
| 4cf9cca116 | |||
| af375a2b5c | |||
| 23a55e025c |
Binary file not shown.
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
# 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
|
||||
182
routes/main.py
182
routes/main.py
@@ -625,6 +625,188 @@ def init_routes(main_bp):
|
||||
'is_valid': is_valid
|
||||
})
|
||||
|
||||
@main_bp.route('/instances/<int:instance_id>/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
|
||||
})
|
||||
|
||||
@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)
|
||||
|
||||
@@ -2014,7 +2014,9 @@ async function downloadDockerCompose(repo, branch) {
|
||||
const result = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
content: result.content
|
||||
content: result.content,
|
||||
commit_hash: result.commit_hash,
|
||||
latest_tag: result.latest_tag
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error downloading docker-compose.yml:', error);
|
||||
|
||||
@@ -40,32 +40,71 @@
|
||||
background-color: #d1ecf1 !important;
|
||||
border-color: #bee5eb !important;
|
||||
}
|
||||
|
||||
.badge.bg-orange {
|
||||
background-color: #fd7e14 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.badge.bg-orange:hover {
|
||||
background-color: #e55a00 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% 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 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="row">
|
||||
<div class="col-12">
|
||||
@@ -84,7 +123,6 @@
|
||||
<th>Main URL</th>
|
||||
<th>Status</th>
|
||||
<th>Version</th>
|
||||
<th>Branch</th>
|
||||
<th>Connection Token</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -126,16 +164,6 @@
|
||||
<span class="badge bg-secondary version-badge">unknown</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% if instance.connection_token %}
|
||||
<span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated">
|
||||
@@ -706,25 +734,37 @@ 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 = '<i class="fas fa-sync-alt"></i> Refresh';
|
||||
refreshButton.className = 'btn btn-outline-primary me-2';
|
||||
refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i> Refresh All';
|
||||
refreshButton.onclick = function() {
|
||||
fetchCompanyNames();
|
||||
};
|
||||
headerButtons.appendChild(refreshButton);
|
||||
|
||||
const versionRefreshButton = document.createElement('button');
|
||||
versionRefreshButton.className = 'btn btn-outline-info';
|
||||
versionRefreshButton.innerHTML = '<i class="fas fa-code-branch"></i> Refresh Versions';
|
||||
versionRefreshButton.onclick = function() {
|
||||
refreshAllVersionInfo();
|
||||
};
|
||||
headerButtons.appendChild(versionRefreshButton);
|
||||
}
|
||||
|
||||
// Wait a short moment to ensure the table is rendered
|
||||
setTimeout(() => {
|
||||
// Check statuses on page load
|
||||
checkAllInstanceStatuses();
|
||||
setTimeout(async () => {
|
||||
// First fetch latest version information
|
||||
await fetchLatestVersion();
|
||||
|
||||
// Fetch company names for all instances
|
||||
// Then check statuses and fetch company names
|
||||
checkAllInstanceStatuses();
|
||||
fetchCompanyNames();
|
||||
}, 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');
|
||||
@@ -768,12 +808,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 +943,193 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to compare semantic versions and determine update type
|
||||
function compareSemanticVersions(currentVersion, latestVersion) {
|
||||
try {
|
||||
// Parse versions into parts (handle cases like "1.0" or "1.0.0")
|
||||
const parseVersion = (version) => {
|
||||
const parts = version.split('.').map(part => {
|
||||
const num = parseInt(part, 10);
|
||||
return isNaN(num) ? 0 : num;
|
||||
});
|
||||
// Ensure we have at least 3 parts (major.minor.patch)
|
||||
while (parts.length < 3) {
|
||||
parts.push(0);
|
||||
}
|
||||
return parts.slice(0, 3); // Only take first 3 parts
|
||||
};
|
||||
|
||||
const current = parseVersion(currentVersion);
|
||||
const latest = parseVersion(latestVersion);
|
||||
|
||||
// Compare major version
|
||||
if (current[0] < latest[0]) {
|
||||
return 'major';
|
||||
}
|
||||
|
||||
// Compare minor version
|
||||
if (current[1] < latest[1]) {
|
||||
return 'minor';
|
||||
}
|
||||
|
||||
// Compare patch version
|
||||
if (current[2] < latest[2]) {
|
||||
return 'patch';
|
||||
}
|
||||
|
||||
// If we get here, current version is newer or equal
|
||||
return 'up_to_date';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error comparing semantic versions:', error);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (adjusted after removing branch)
|
||||
|
||||
// Show loading state
|
||||
if (versionCell) {
|
||||
versionCell.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 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') {
|
||||
// Get the latest version for comparison
|
||||
const latestVersionBadge = document.getElementById('latestVersionBadge');
|
||||
let latestVersion = latestVersionBadge ? latestVersionBadge.textContent.replace('Loading...', '').trim() : null;
|
||||
|
||||
// If latest version is not available yet, wait a bit and try again
|
||||
if (!latestVersion || latestVersion === '') {
|
||||
// Show loading state while waiting for latest version
|
||||
versionCell.innerHTML = `
|
||||
<span class="badge bg-secondary version-badge" data-bs-toggle="tooltip"
|
||||
title="App Version: ${appVersion}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}<br>Waiting for latest version...">
|
||||
<i class="fas fa-spinner fa-spin me-1"></i>${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
|
||||
</span>`;
|
||||
|
||||
// Wait a bit and retry the comparison
|
||||
setTimeout(() => {
|
||||
fetchVersionInfo(instanceUrl, instanceId);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Exact match - green
|
||||
badgeClass = 'bg-success';
|
||||
statusIcon = 'fas fa-check-circle';
|
||||
tooltipText += '<br><strong>✅ Up to date</strong>';
|
||||
} else if (latestVersion && appVersion !== latestVersion) {
|
||||
// Compare semantic versions
|
||||
const versionComparison = compareSemanticVersions(appVersion, latestVersion);
|
||||
|
||||
switch (versionComparison) {
|
||||
case 'patch':
|
||||
// Only patch version different - yellow
|
||||
badgeClass = 'bg-warning';
|
||||
statusIcon = 'fas fa-exclamation-triangle';
|
||||
tooltipText += `<br><strong>🟡 Patch update available (Latest: ${latestVersion})</strong>`;
|
||||
break;
|
||||
case 'minor':
|
||||
// Minor version different - orange
|
||||
badgeClass = 'bg-orange';
|
||||
statusIcon = 'fas fa-exclamation-triangle';
|
||||
tooltipText += `<br><strong>🟠 Minor update available (Latest: ${latestVersion})</strong>`;
|
||||
break;
|
||||
case 'major':
|
||||
// Major version different - red
|
||||
badgeClass = 'bg-danger';
|
||||
statusIcon = 'fas fa-exclamation-triangle';
|
||||
tooltipText += `<br><strong>🔴 Major update available (Latest: ${latestVersion})</strong>`;
|
||||
break;
|
||||
default:
|
||||
// Unknown format or comparison failed - red
|
||||
badgeClass = 'bg-danger';
|
||||
statusIcon = 'fas fa-exclamation-triangle';
|
||||
tooltipText += `<br><strong>🔴 Outdated (Latest: ${latestVersion})</strong>`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
versionCell.innerHTML = `
|
||||
<span class="badge ${badgeClass} version-badge" data-bs-toggle="tooltip"
|
||||
title="${tooltipText}">
|
||||
<i class="${statusIcon} me-1"></i>${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
|
||||
</span>`;
|
||||
} else {
|
||||
versionCell.innerHTML = '<span class="badge bg-secondary version-badge">unknown</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Update tooltips
|
||||
const versionBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
|
||||
|
||||
if (versionBadge) {
|
||||
new bootstrap.Tooltip(versionBadge);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching version info for instance ${instanceId}:`, error);
|
||||
|
||||
// Show error state
|
||||
if (versionCell) {
|
||||
versionCell.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
|
||||
const errorBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
|
||||
if (errorBadge) {
|
||||
new bootstrap.Tooltip(errorBadge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +1217,30 @@ 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
|
||||
];
|
||||
|
||||
cells.forEach(cell => {
|
||||
@@ -1043,7 +1260,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 +2103,134 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user