version v3

This commit is contained in:
2025-06-23 15:17:17 +02:00
parent 23a55e025c
commit af375a2b5c
4 changed files with 292 additions and 45 deletions

View File

@@ -563,4 +563,31 @@ def generate_password_reset_token(current_user, user_id):
'reset_url': reset_url, 'reset_url': reset_url,
'expires_at': reset_token.expires_at.isoformat(), 'expires_at': reset_token.expires_at.isoformat(),
'user_email': user.email '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

View File

@@ -625,6 +625,93 @@ def init_routes(main_bp):
'is_valid': is_valid '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
})
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

@@ -706,12 +706,20 @@ document.addEventListener('DOMContentLoaded', function() {
const headerButtons = document.querySelector('.header-buttons'); const headerButtons = document.querySelector('.header-buttons');
if (headerButtons) { if (headerButtons) {
const refreshButton = document.createElement('button'); const refreshButton = document.createElement('button');
refreshButton.className = 'btn btn-outline-primary'; refreshButton.className = 'btn btn-outline-primary me-2';
refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i> Refresh'; refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i> Refresh All';
refreshButton.onclick = function() { refreshButton.onclick = function() {
fetchCompanyNames(); fetchCompanyNames();
}; };
headerButtons.appendChild(refreshButton); 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 // Wait a short moment to ensure the table is rendered
@@ -768,12 +776,25 @@ document.addEventListener('DOMContentLoaded', function() {
updateColorPreview(); updateColorPreview();
}); });
// Function to check status of all instances // Function to check all instance statuses
async function checkAllInstanceStatuses() { async function checkAllInstanceStatuses() {
const statusBadges = document.querySelectorAll('[data-instance-id]'); console.log('Checking all instance statuses...');
for (const badge of statusBadges) { const instances = document.querySelectorAll('[data-instance-id]');
for (const badge of instances) {
const instanceId = badge.dataset.instanceId; const instanceId = badge.dataset.instanceId;
await checkInstanceStatus(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 = '<i class="fas fa-spinner fa-spin"></i> Loading...';
}
if (branchCell) {
branchCell.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') {
versionCell.innerHTML = `
<span class="badge bg-info version-badge" data-bs-toggle="tooltip"
title="App Version: ${appVersion}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}">
${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
</span>`;
} else {
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
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 = `
<span class="text-warning" data-bs-toggle="tooltip" title="Error: ${error.message}">
<i class="fas fa-exclamation-triangle"></i> Error
</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
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 // Function to fetch company name from instance settings
async function fetchCompanyName(instanceUrl, instanceId) { async function fetchCompanyName(instanceUrl, instanceId) {
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr'); 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 // Function to fetch company names for all instances
async function fetchCompanyNames() { async function fetchCompanyNames() {
console.log('Starting fetchCompanyNames...');
const instances = document.querySelectorAll('[data-instance-id]'); const instances = document.querySelectorAll('[data-instance-id]');
const loadingPromises = []; const loadingPromises = [];
console.log('Starting to fetch company names and stats for all instances'); for (const badge of instances) {
const instanceId = badge.dataset.instanceId;
for (const instance of instances) { const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href;
const instanceId = instance.dataset.instanceId; const apiKey = badge.dataset.token;
const row = instance.closest('tr');
// Debug: Log all cells in the row if (instanceUrl && apiKey) {
console.log(`Row for instance ${instanceId}:`, { console.log(`Fetching data for instance ${instanceId}`);
cells: Array.from(row.querySelectorAll('td')).map(td => ({ loadingPromises.push(
text: td.textContent.trim(), fetchCompanyName(instanceUrl, instanceId),
html: td.innerHTML.trim() fetchVersionInfo(instanceUrl, instanceId) // Add version info fetching
})) );
});
// 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));
} else { } else {
const row = badge.closest('tr');
const cells = [ const cells = [
row.querySelector('td:nth-child(2)'), // Company row.querySelector('td:nth-child(2)'), // Company
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(10)') // Branch
]; ];
cells.forEach(cell => { cells.forEach(cell => {
@@ -1043,7 +1151,7 @@ async function fetchCompanyNames() {
try { try {
await Promise.all(loadingPromises); 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) { } catch (error) {
console.error('Error in fetchCompanyNames:', error); console.error('Error in fetchCompanyNames:', error);
} }
@@ -1886,5 +1994,30 @@ function launchInstance() {
// Redirect to the launch progress page // Redirect to the launch progress page
window.location.href = '/instances/launch-progress'; 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);
}
}
</script> </script>
{% endblock %} {% endblock %}