Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b598f2966 | |||
| 77032062a1 |
@@ -929,8 +929,8 @@ def deploy_stack():
|
||||
def check_stack_status():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'stack_name' not in data:
|
||||
return jsonify({'error': 'Missing stack_name field'}), 400
|
||||
if not data or ('stack_name' not in data and 'stack_id' not in data):
|
||||
return jsonify({'error': 'Missing stack_name or stack_id field'}), 400
|
||||
|
||||
# Get Portainer settings
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
@@ -956,35 +956,54 @@ def check_stack_status():
|
||||
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
|
||||
# Get stack information
|
||||
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
||||
stacks_response = requests.get(
|
||||
stacks_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'filters': json.dumps({'Name': data['stack_name']})},
|
||||
timeout=30
|
||||
)
|
||||
# Get stack information - support both stack_name and stack_id
|
||||
if 'stack_id' in data:
|
||||
# Get stack by ID
|
||||
stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||
stack_response = requests.get(
|
||||
stack_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'endpointId': endpoint_id},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not stacks_response.ok:
|
||||
return jsonify({'error': 'Failed to get stack information'}), 500
|
||||
if not stack_response.ok:
|
||||
return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404
|
||||
|
||||
stacks = stacks_response.json()
|
||||
target_stack = None
|
||||
|
||||
for stack in stacks:
|
||||
if stack['Name'] == data['stack_name']:
|
||||
target_stack = stack
|
||||
break
|
||||
target_stack = stack_response.json()
|
||||
else:
|
||||
# Get stack by name (existing logic)
|
||||
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
||||
stacks_response = requests.get(
|
||||
stacks_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'filters': json.dumps({'Name': data['stack_name']})},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not target_stack:
|
||||
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
|
||||
if not stacks_response.ok:
|
||||
return jsonify({'error': 'Failed to get stack information'}), 500
|
||||
|
||||
stacks = stacks_response.json()
|
||||
target_stack = None
|
||||
|
||||
for stack in stacks:
|
||||
if stack['Name'] == data['stack_name']:
|
||||
target_stack = stack
|
||||
break
|
||||
|
||||
if not target_stack:
|
||||
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
|
||||
|
||||
# Get stack services to check their status
|
||||
services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services"
|
||||
current_app.logger.info(f"Checking services for stack {data['stack_name']} at endpoint {endpoint_id}")
|
||||
current_app.logger.info(f"Checking services for stack {target_stack['Name']} at endpoint {endpoint_id}")
|
||||
|
||||
try:
|
||||
services_response = requests.get(
|
||||
@@ -993,7 +1012,7 @@ def check_stack_status():
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
|
||||
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={target_stack["Name"]}'})},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
@@ -1001,46 +1020,40 @@ def check_stack_status():
|
||||
|
||||
if services_response.ok:
|
||||
services = services_response.json()
|
||||
current_app.logger.info(f"Found {len(services)} services for stack {data['stack_name']}")
|
||||
|
||||
# Check if all services are running
|
||||
all_running = True
|
||||
service_statuses = []
|
||||
|
||||
for service in services:
|
||||
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
|
||||
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
|
||||
|
||||
service_status = {
|
||||
service_statuses.append({
|
||||
'name': service.get('Spec', {}).get('Name', 'Unknown'),
|
||||
'replicas_expected': replicas_running,
|
||||
'replicas_running': replicas_actual,
|
||||
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
|
||||
}
|
||||
|
||||
service_statuses.append(service_status)
|
||||
|
||||
if replicas_actual < replicas_running:
|
||||
all_running = False
|
||||
|
||||
'replicas': service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0),
|
||||
'running_replicas': service.get('ServiceStatus', {}).get('RunningTasks', 0),
|
||||
'desired_replicas': service.get('ServiceStatus', {}).get('DesiredTasks', 0)
|
||||
})
|
||||
|
||||
# Determine overall stack status
|
||||
if all_running and len(services) > 0:
|
||||
status = 'active'
|
||||
elif len(services) > 0:
|
||||
status = 'partial'
|
||||
if not service_statuses:
|
||||
status = 'starting' # No services found yet
|
||||
else:
|
||||
status = 'inactive'
|
||||
all_running = all(s['running_replicas'] >= s['desired_replicas'] for s in service_statuses if s['desired_replicas'] > 0)
|
||||
any_running = any(s['running_replicas'] > 0 for s in service_statuses)
|
||||
|
||||
if all_running:
|
||||
status = 'active'
|
||||
elif any_running:
|
||||
status = 'partial'
|
||||
else:
|
||||
status = 'inactive'
|
||||
else:
|
||||
# Services API failed, but stack exists - assume it's still starting up
|
||||
current_app.logger.warning(f"Failed to get services for stack {data['stack_name']}: {services_response.status_code} - {services_response.text}")
|
||||
current_app.logger.warning(f"Failed to get services for stack {target_stack['Name']}: {services_response.status_code} - {services_response.text}")
|
||||
|
||||
# Provide more specific error context
|
||||
if services_response.status_code == 404:
|
||||
current_app.logger.info(f"Services endpoint not found for stack {data['stack_name']} - stack may still be initializing")
|
||||
current_app.logger.info(f"Services endpoint not found for stack {target_stack['Name']} - stack may still be initializing")
|
||||
elif services_response.status_code == 403:
|
||||
current_app.logger.warning(f"Access denied to services for stack {data['stack_name']} - check Portainer permissions")
|
||||
current_app.logger.warning(f"Access denied to services for stack {target_stack['Name']} - check Portainer permissions")
|
||||
elif services_response.status_code >= 500:
|
||||
current_app.logger.warning(f"Portainer server error when getting services for stack {data['stack_name']}")
|
||||
current_app.logger.warning(f"Portainer server error when getting services for stack {target_stack['Name']}")
|
||||
|
||||
services = []
|
||||
service_statuses = []
|
||||
@@ -1048,7 +1061,7 @@ def check_stack_status():
|
||||
|
||||
except Exception as e:
|
||||
# Exception occurred while getting services, but stack exists
|
||||
current_app.logger.warning(f"Exception getting services for stack {data['stack_name']}: {str(e)}")
|
||||
current_app.logger.warning(f"Exception getting services for stack {target_stack['Name']}: {str(e)}")
|
||||
services = []
|
||||
service_statuses = []
|
||||
status = 'starting' # Stack exists but services not available yet
|
||||
@@ -1056,14 +1069,10 @@ def check_stack_status():
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'stack_name': data['stack_name'],
|
||||
'name': target_stack['Name'],
|
||||
'stack_id': target_stack['Id'],
|
||||
'status': status,
|
||||
'services': service_statuses,
|
||||
'total_services': len(services),
|
||||
'running_services': len([s for s in service_statuses if s['status'] == 'running']),
|
||||
'stack_created_at': target_stack.get('CreatedAt', 'unknown'),
|
||||
'stack_updated_at': target_stack.get('UpdatedAt', 'unknown')
|
||||
'services': service_statuses
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1854,4 +1863,216 @@ def copy_smtp_settings():
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error copying SMTP settings: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@launch_api.route('/update-stack', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def update_stack():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'stack_id' not in data:
|
||||
return jsonify({'error': 'Missing required fields'}), 400
|
||||
|
||||
# Get Portainer settings
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
if not portainer_settings:
|
||||
return jsonify({'error': 'Portainer settings not configured'}), 400
|
||||
|
||||
# Define timeout early to ensure it's available throughout the function
|
||||
stack_timeout = current_app.config.get('STACK_DEPLOYMENT_TIMEOUT', 300) # Default to 5 minutes
|
||||
|
||||
# Verify Portainer authentication
|
||||
auth_response = requests.get(
|
||||
f"{portainer_settings['url'].rstrip('/')}/api/status",
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
timeout=30 # 30 seconds timeout for status check
|
||||
)
|
||||
|
||||
if not auth_response.ok:
|
||||
current_app.logger.error(f"Portainer authentication failed: {auth_response.text}")
|
||||
return jsonify({'error': 'Failed to authenticate with Portainer'}), 401
|
||||
|
||||
# Get Portainer endpoint ID (assuming it's the first endpoint)
|
||||
endpoint_response = requests.get(
|
||||
f"{portainer_settings['url'].rstrip('/')}/api/endpoints",
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
timeout=30 # 30 seconds timeout for endpoint check
|
||||
)
|
||||
if not endpoint_response.ok:
|
||||
error_text = endpoint_response.text
|
||||
try:
|
||||
error_json = endpoint_response.json()
|
||||
error_text = error_json.get('message', error_text)
|
||||
except:
|
||||
pass
|
||||
return jsonify({'error': f'Failed to get Portainer endpoints: {error_text}'}), 500
|
||||
|
||||
endpoints = endpoint_response.json()
|
||||
if not endpoints:
|
||||
return jsonify({'error': 'No Portainer endpoints found'}), 400
|
||||
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
|
||||
# Log the request data
|
||||
current_app.logger.info(f"Updating stack with ID: {data['stack_id']}")
|
||||
current_app.logger.info(f"Using endpoint ID: {endpoint_id}")
|
||||
current_app.logger.info(f"Using timeout: {stack_timeout} seconds")
|
||||
|
||||
# First, verify the stack exists and get its current configuration
|
||||
stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||
stack_response = requests.get(
|
||||
stack_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'endpointId': endpoint_id},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not stack_response.ok:
|
||||
return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404
|
||||
|
||||
stack_info = stack_response.json()
|
||||
current_app.logger.info(f"Found existing stack: {stack_info['Name']} (ID: {stack_info['Id']})")
|
||||
|
||||
# Get the current stack file content from Portainer
|
||||
stack_file_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}/file"
|
||||
stack_file_response = requests.get(
|
||||
stack_file_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'endpointId': endpoint_id},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not stack_file_response.ok:
|
||||
current_app.logger.error(f"Failed to get stack file: {stack_file_response.text}")
|
||||
return jsonify({'error': f'Failed to get existing stack file: {stack_file_response.text}'}), 500
|
||||
|
||||
stack_file_data = stack_file_response.json()
|
||||
current_stack_file_content = stack_file_data.get('StackFileContent')
|
||||
|
||||
if not current_stack_file_content:
|
||||
current_app.logger.error("No StackFileContent found in existing stack")
|
||||
return jsonify({'error': 'No existing stack file content found'}), 500
|
||||
|
||||
current_app.logger.info("Retrieved existing stack file content")
|
||||
|
||||
# Get existing environment variables from the stack
|
||||
existing_env_vars = stack_file_data.get('Env', [])
|
||||
current_app.logger.info(f"Retrieved {len(existing_env_vars)} existing environment variables")
|
||||
|
||||
# Create a dictionary of existing environment variables for easy lookup
|
||||
existing_env_dict = {env['name']: env['value'] for env in existing_env_vars}
|
||||
current_app.logger.info(f"Existing environment variables: {list(existing_env_dict.keys())}")
|
||||
|
||||
# Get new environment variables from the request
|
||||
new_env_vars = data.get('Env', [])
|
||||
current_app.logger.info(f"New environment variables to update: {[env['name'] for env in new_env_vars]}")
|
||||
|
||||
# Merge existing and new environment variables
|
||||
# Start with existing variables
|
||||
merged_env_vars = existing_env_vars.copy()
|
||||
|
||||
# Update with new variables (this will overwrite existing ones with the same name)
|
||||
for new_env in new_env_vars:
|
||||
# Find if this environment variable already exists
|
||||
existing_index = None
|
||||
for i, existing_env in enumerate(merged_env_vars):
|
||||
if existing_env['name'] == new_env['name']:
|
||||
existing_index = i
|
||||
break
|
||||
|
||||
if existing_index is not None:
|
||||
# Update existing variable
|
||||
merged_env_vars[existing_index]['value'] = new_env['value']
|
||||
current_app.logger.info(f"Updated environment variable: {new_env['name']} = {new_env['value']}")
|
||||
else:
|
||||
# Add new variable
|
||||
merged_env_vars.append(new_env)
|
||||
current_app.logger.info(f"Added new environment variable: {new_env['name']} = {new_env['value']}")
|
||||
|
||||
current_app.logger.info(f"Final merged environment variables: {[env['name'] for env in merged_env_vars]}")
|
||||
|
||||
# Update the stack using Portainer's update API
|
||||
update_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||
current_app.logger.info(f"Making update request to: {update_url}")
|
||||
|
||||
# Prepare the request body for stack update
|
||||
request_body = {
|
||||
'StackFileContent': current_stack_file_content, # Use existing stack file content
|
||||
'Env': merged_env_vars # Use merged environment variables
|
||||
}
|
||||
|
||||
# If new StackFileContent is provided, use it instead
|
||||
if 'StackFileContent' in data:
|
||||
request_body['StackFileContent'] = data['StackFileContent']
|
||||
current_app.logger.info("Using provided StackFileContent for update")
|
||||
else:
|
||||
current_app.logger.info("Using existing StackFileContent for update")
|
||||
|
||||
# Add endpointId as a query parameter
|
||||
params = {'endpointId': endpoint_id}
|
||||
|
||||
# Use a configurable timeout for stack update initiation
|
||||
update_response = requests.put(
|
||||
update_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params=params,
|
||||
json=request_body,
|
||||
timeout=stack_timeout # Use configurable timeout
|
||||
)
|
||||
|
||||
# Log the response details
|
||||
current_app.logger.info(f"Update response status: {update_response.status_code}")
|
||||
current_app.logger.info(f"Update response headers: {dict(update_response.headers)}")
|
||||
|
||||
response_text = update_response.text
|
||||
current_app.logger.info(f"Update response body: {response_text}")
|
||||
|
||||
if not update_response.ok:
|
||||
error_message = response_text
|
||||
try:
|
||||
error_json = update_response.json()
|
||||
error_message = error_json.get('message', error_message)
|
||||
except:
|
||||
pass
|
||||
return jsonify({'error': f'Failed to update stack: {error_message}'}), 500
|
||||
|
||||
# Stack update initiated successfully
|
||||
current_app.logger.info(f"Stack update initiated: {stack_info['Name']} (ID: {stack_info['Id']})")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'name': stack_info['Name'],
|
||||
'id': stack_info['Id'],
|
||||
'status': 'updating'
|
||||
}
|
||||
})
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
current_app.logger.error(f"Request timed out after {stack_timeout} seconds while initiating stack update")
|
||||
current_app.logger.error(f"Stack ID: {data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown'}")
|
||||
current_app.logger.error(f"Portainer URL: {portainer_settings.get('url', 'unknown') if 'portainer_settings' in locals() else 'unknown'}")
|
||||
return jsonify({
|
||||
'error': f'Request timed out after {stack_timeout} seconds while initiating stack update. The operation may still be in progress.',
|
||||
'timeout_seconds': stack_timeout,
|
||||
'stack_id': data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown'
|
||||
}), 504
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error updating stack: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -1395,7 +1395,7 @@ async function startUpdate(data) {
|
||||
// Step 3: Deploy Updated Stack
|
||||
await updateStep(3, 'Deploying Updated Stack', 'Deploying the updated application stack...');
|
||||
|
||||
// Get the existing instance information to extract port
|
||||
// Get the existing instance information to extract port and stack details
|
||||
const instanceResponse = await fetch(`/api/instances/${data.instanceId}`);
|
||||
if (!instanceResponse.ok) {
|
||||
throw new Error('Failed to get instance information');
|
||||
@@ -1403,15 +1403,18 @@ async function startUpdate(data) {
|
||||
const instanceData = await instanceResponse.json();
|
||||
const port = instanceData.instance.name; // Assuming the instance name is the port
|
||||
|
||||
// Generate new stack name with timestamp
|
||||
const newStackName = generateStackName(port);
|
||||
// Check if we have an existing stack ID
|
||||
if (!instanceData.instance.portainer_stack_id) {
|
||||
throw new Error('No existing stack found for this instance. Cannot perform update.');
|
||||
}
|
||||
|
||||
const stackResult = await deployStack(dockerComposeResult.content, newStackName, port);
|
||||
// Update the existing stack instead of creating a new one
|
||||
const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port, instanceData.instance);
|
||||
if (!stackResult.success) {
|
||||
throw new Error(`Failed to deploy updated stack: ${stackResult.error}`);
|
||||
throw new Error(`Failed to update stack: ${stackResult.error}`);
|
||||
}
|
||||
launchReport.steps.push({
|
||||
step: 'Stack Deployment',
|
||||
step: 'Stack Update',
|
||||
status: 'success',
|
||||
details: stackResult
|
||||
});
|
||||
@@ -1422,8 +1425,8 @@ async function startUpdate(data) {
|
||||
name: instanceData.instance.name,
|
||||
port: port,
|
||||
domains: instanceData.instance.main_url ? [instanceData.instance.main_url.replace(/^https?:\/\//, '')] : [],
|
||||
stack_id: stackResult.data.id || null,
|
||||
stack_name: newStackName,
|
||||
stack_id: instanceData.instance.portainer_stack_id, // Keep the same stack ID
|
||||
stack_name: instanceData.instance.portainer_stack_name, // Keep the same stack name
|
||||
status: stackResult.data.status,
|
||||
repository: data.repository,
|
||||
branch: data.branch,
|
||||
@@ -1468,7 +1471,7 @@ async function startUpdate(data) {
|
||||
successDetails.className = 'mt-3';
|
||||
|
||||
// Calculate the volume names based on the stack name
|
||||
const stackNameParts = newStackName.split('_');
|
||||
const stackNameParts = stackResult.data.name.split('_');
|
||||
let volumeNames = [];
|
||||
if (stackNameParts.length >= 3) {
|
||||
const timestamp = stackNameParts.slice(2).join('_');
|
||||
@@ -1482,7 +1485,7 @@ async function startUpdate(data) {
|
||||
successDetails.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="fas fa-check-circle me-2"></i>Update Completed Successfully!</h6>
|
||||
<p class="mb-2">Your instance has been updated with the latest version from the repository.</p>
|
||||
<p class="mb-2">Your instance has been updated with the latest version from the repository. All existing data and volumes have been preserved.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>Repository:</strong> ${data.repository}<br>
|
||||
@@ -1490,18 +1493,16 @@ async function startUpdate(data) {
|
||||
<strong>New Version:</strong> ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>New Stack Name:</strong> ${newStackName}<br>
|
||||
<strong>Stack Name:</strong> ${stackResult.data.name}<br>
|
||||
<strong>Instance URL:</strong> <a href="${instanceData.instance.main_url}" target="_blank">${instanceData.instance.main_url}</a>
|
||||
</div>
|
||||
</div>
|
||||
${volumeNames.length > 0 ? `
|
||||
<div class="mt-3">
|
||||
<strong>New Volume Names:</strong>
|
||||
<div class="small mt-1">
|
||||
${volumeNames.map(name => `<code class="text-primary">${name}</code>`).join('<br>')}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Data Preservation:</strong> All existing data, volumes, and configurations have been preserved during this update.
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
successStep.querySelector('.step-content').appendChild(successDetails);
|
||||
@@ -2445,70 +2446,95 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
|
||||
const data = await statusResponse.json();
|
||||
|
||||
// Update the health check step
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[10]; // Adjust index based on your steps
|
||||
healthStepElement.classList.remove('active');
|
||||
healthStepElement.classList.add('completed');
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
// Update the health check step - check if we're in update mode or launch mode
|
||||
const isUpdate = window.isUpdate;
|
||||
const healthStepIndex = isUpdate ? 4 : 10; // Different indices for update vs launch
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
|
||||
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed)`;
|
||||
if (healthStepElement) {
|
||||
healthStepElement.classList.remove('active');
|
||||
healthStepElement.classList.add('completed');
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
|
||||
// Update progress bar to 100%
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-success');
|
||||
progressText.textContent = `Health check completed successfully in ${elapsedTime}s`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
} else if (data.status === 'inactive') {
|
||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
||||
lastKnownStatus = 'inactive';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (data.status === 'starting') {
|
||||
console.log(`Stack ${stackName} is starting up, continuing to poll...`);
|
||||
lastKnownStatus = 'starting';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is initializing (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed)`;
|
||||
|
||||
// Update progress bar to 100% if it exists
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-success');
|
||||
progressText.textContent = `Health check completed successfully in ${elapsedTime}s`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
} else if (data.status === 'inactive') {
|
||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
||||
lastKnownStatus = 'inactive';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (data.status === 'starting') {
|
||||
console.log(`Stack ${stackName} is starting up, continuing to poll...`);
|
||||
lastKnownStatus = 'starting';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is initializing (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Instance is not healthy');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Instance is not healthy');
|
||||
// If step element doesn't exist, just log the status
|
||||
console.log(`Health check - Instance status: ${data.status}`);
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Health check attempt ${currentAttempt} failed:`, error);
|
||||
|
||||
// Update status to show current attempt and elapsed time
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[10];
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed): ${error.message}`;
|
||||
// Update status to show current attempt and elapsed time - check if step element exists
|
||||
const isUpdate = window.isUpdate;
|
||||
const healthStepIndex = isUpdate ? 4 : 10;
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const progressPercent = Math.min((currentAttempt / maxRetries) * 100, 100);
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
progressBar.textContent = `${Math.round(progressPercent)}%`;
|
||||
progressText.textContent = `Attempt ${currentAttempt}/${maxRetries} (${elapsedTime}s elapsed)`;
|
||||
if (healthStepElement) {
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed): ${error.message}`;
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const progressPercent = Math.min((currentAttempt / maxRetries) * 100, 100);
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
progressBar.textContent = `${Math.round(progressPercent)}%`;
|
||||
progressText.textContent = `Attempt ${currentAttempt}/${maxRetries} (${elapsedTime}s elapsed)`;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAttempt === maxRetries || (Date.now() - startTime > maxTotalTime)) {
|
||||
// Update progress bar to show failure
|
||||
// Update progress bar to show failure if it exists
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-danger');
|
||||
progressText.textContent = `Health check failed after ${currentAttempt} attempts (${elapsedTime}s)`;
|
||||
@@ -2516,7 +2542,7 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Health check failed after ${currentAttempt} attempts (${elapsedTime}s): ${error.message}`
|
||||
error: `Health check failed after ${currentAttempt} attempts (${Math.round((Date.now() - startTime) / 1000)}s): ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2524,7 +2550,8 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
await new Promise(resolve => setTimeout(resolve, baseDelay));
|
||||
currentAttempt++;
|
||||
|
||||
// Update progress bar in real-time
|
||||
// Update progress bar in real-time if it exists
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
updateHealthProgress(currentAttempt, maxRetries, elapsedTime);
|
||||
}
|
||||
}
|
||||
@@ -2542,127 +2569,6 @@ function updateHealthProgress(currentAttempt, maxRetries, elapsedTime) {
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateInstance(instanceUrl, instanceId) {
|
||||
try {
|
||||
// First check if instance is already authenticated
|
||||
const instancesResponse = await fetch('/instances');
|
||||
const text = await instancesResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, 'text/html');
|
||||
|
||||
// Find the instance with matching URL
|
||||
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
|
||||
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
|
||||
return urlCell && urlCell.textContent.trim() === instanceUrl;
|
||||
});
|
||||
|
||||
if (!instanceRow) {
|
||||
throw new Error('Instance not found in database');
|
||||
}
|
||||
|
||||
// Get the instance ID from the status badge's data attribute
|
||||
const statusBadge = instanceRow.querySelector('[data-instance-id]');
|
||||
if (!statusBadge) {
|
||||
throw new Error('Could not find instance ID');
|
||||
}
|
||||
|
||||
const dbInstanceId = statusBadge.dataset.instanceId;
|
||||
|
||||
// Check if already authenticated
|
||||
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
|
||||
if (!authStatusResponse.ok) {
|
||||
throw new Error('Failed to check authentication status');
|
||||
}
|
||||
|
||||
const authStatus = await authStatusResponse.json();
|
||||
if (authStatus.authenticated) {
|
||||
console.log('Instance is already authenticated');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Instance is already authenticated',
|
||||
alreadyAuthenticated: true
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
|
||||
|
||||
// First login to get token
|
||||
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'administrator@docupulse.com',
|
||||
password: 'changeme'
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
const errorText = await loginResponse.text();
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
if (loginData.status !== 'success' || !loginData.token) {
|
||||
throw new Error('Login failed: Invalid response from server');
|
||||
}
|
||||
|
||||
const token = loginData.token;
|
||||
|
||||
// Then create management API key
|
||||
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `Connection from ${window.location.hostname}`
|
||||
})
|
||||
});
|
||||
|
||||
if (!keyResponse.ok) {
|
||||
const errorText = await keyResponse.text();
|
||||
throw new Error(`Failed to create API key: ${errorText}`);
|
||||
}
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
if (!keyData.api_key) {
|
||||
throw new Error('No API key received from server');
|
||||
}
|
||||
|
||||
// Save the token to our database
|
||||
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ token: keyData.api_key })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to save token: ${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully authenticated instance',
|
||||
alreadyAuthenticated: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCompanyInformation(instanceUrl, company) {
|
||||
try {
|
||||
console.log('Applying company information to:', instanceUrl);
|
||||
@@ -3129,26 +3035,26 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
||||
}
|
||||
|
||||
// Function to poll stack status
|
||||
async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
async function pollStackStatus(stackIdentifier, maxWaitTime = 15 * 60 * 1000) {
|
||||
const startTime = Date.now();
|
||||
const pollInterval = 5000; // 5 seconds
|
||||
let attempts = 0;
|
||||
let lastKnownStatus = 'unknown';
|
||||
|
||||
// Validate stack name
|
||||
if (!stackName || typeof stackName !== 'string') {
|
||||
console.error('Invalid stack name provided to pollStackStatus:', stackName);
|
||||
// Validate stack identifier (can be name or ID)
|
||||
if (!stackIdentifier || typeof stackIdentifier !== 'string') {
|
||||
console.error('Invalid stack identifier provided to pollStackStatus:', stackIdentifier);
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid stack name: ${stackName}`,
|
||||
error: `Invalid stack identifier: ${stackIdentifier}`,
|
||||
data: {
|
||||
name: stackName,
|
||||
name: stackIdentifier,
|
||||
status: 'error'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`);
|
||||
console.log(`Starting to poll stack status for: ${stackIdentifier} (max wait: ${maxWaitTime / 1000}s)`);
|
||||
|
||||
// Update progress indicator
|
||||
const progressBar = document.getElementById('launchProgress');
|
||||
@@ -3156,7 +3062,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime) {
|
||||
attempts++;
|
||||
console.log(`Polling attempt ${attempts} for stack: ${stackName}`);
|
||||
console.log(`Polling attempt ${attempts} for stack: ${stackIdentifier}`);
|
||||
|
||||
// Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0%
|
||||
const elapsed = Date.now() - startTime;
|
||||
@@ -3168,9 +3074,12 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
}
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
stack_name: stackName
|
||||
};
|
||||
// Determine if this is a stack ID (numeric) or stack name
|
||||
const isStackId = /^\d+$/.test(stackIdentifier);
|
||||
const requestBody = isStackId ?
|
||||
{ stack_id: stackIdentifier } :
|
||||
{ stack_name: stackIdentifier };
|
||||
|
||||
console.log(`Sending stack status check request:`, requestBody);
|
||||
|
||||
const response = await fetch('/api/admin/check-stack-status', {
|
||||
@@ -3188,7 +3097,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
console.log(`Stack status check result:`, result);
|
||||
|
||||
if (result.data && result.data.status === 'active') {
|
||||
console.log(`Stack ${stackName} is now active!`);
|
||||
console.log(`Stack ${stackIdentifier} is now active!`);
|
||||
// Update progress to 100%
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
@@ -3200,25 +3109,25 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: stackName,
|
||||
id: result.data.stack_id,
|
||||
name: result.data.name || stackIdentifier,
|
||||
id: result.data.stack_id || stackIdentifier,
|
||||
status: 'active'
|
||||
}
|
||||
};
|
||||
} else if (result.data && result.data.status === 'partial') {
|
||||
console.log(`Stack ${stackName} is partially running, continuing to poll...`);
|
||||
console.log(`Stack ${stackIdentifier} is partially running, continuing to poll...`);
|
||||
lastKnownStatus = 'partial';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is partially running (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (result.data && result.data.status === 'inactive') {
|
||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
||||
console.log(`Stack ${stackIdentifier} is inactive, continuing to poll...`);
|
||||
lastKnownStatus = 'inactive';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (result.data && result.data.status === 'starting') {
|
||||
console.log(`Stack ${stackName} exists and is starting up - continuing to next step`);
|
||||
console.log(`Stack ${stackIdentifier} exists and is starting up - continuing to next step`);
|
||||
// Stack exists, we can continue - no need to wait for all services
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
@@ -3230,20 +3139,20 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: stackName,
|
||||
id: result.data.stack_id,
|
||||
name: result.data.name || stackIdentifier,
|
||||
id: result.data.stack_id || stackIdentifier,
|
||||
status: 'starting'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log(`Stack ${stackName} status unknown, continuing to poll...`);
|
||||
console.log(`Stack ${stackIdentifier} status unknown, continuing to poll...`);
|
||||
lastKnownStatus = 'unknown';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Checking stack status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
console.log(`Stack ${stackName} not found yet, continuing to poll...`);
|
||||
console.log(`Stack ${stackIdentifier} not found yet, continuing to poll...`);
|
||||
lastKnownStatus = 'not_found';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
@@ -3280,11 +3189,11 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
||||
success: false,
|
||||
error: `Stack deployment timed out after ${Math.round(maxWaitTime / 1000)} seconds${statusMessage}. The stack may still be deploying in the background.`,
|
||||
data: {
|
||||
name: stackName,
|
||||
name: stackIdentifier,
|
||||
status: lastKnownStatus
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to generate unique stack names with timestamp
|
||||
function generateStackName(port) {
|
||||
@@ -3296,4 +3205,284 @@ function generateStackName(port) {
|
||||
now.getMinutes().toString().padStart(2, '0') +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
return `docupulse_${port}_${timestamp}`;
|
||||
}
|
||||
|
||||
// Add new function to update existing stack
|
||||
async function updateStack(dockerComposeContent, stackId, port, instanceData = null) {
|
||||
try {
|
||||
console.log('Updating existing stack:', stackId);
|
||||
console.log('Port:', port);
|
||||
console.log('Modified docker-compose content length:', dockerComposeContent.length);
|
||||
|
||||
// For updates, we only need to update version-related environment variables
|
||||
// All other environment variables (pricing tiers, quotas, etc.) should be preserved
|
||||
// We also preserve the existing docker-compose configuration
|
||||
const envVars = [
|
||||
{
|
||||
name: 'APP_VERSION',
|
||||
value: window.currentDeploymentVersion || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_COMMIT',
|
||||
value: window.currentDeploymentCommit || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_BRANCH',
|
||||
value: window.currentDeploymentBranch || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'DEPLOYED_AT',
|
||||
value: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Updating stack with version environment variables:', envVars);
|
||||
console.log('Preserving existing docker-compose configuration');
|
||||
|
||||
const response = await fetch('/api/admin/update-stack', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stack_id: stackId,
|
||||
// Don't send StackFileContent during updates - preserve existing configuration
|
||||
Env: envVars
|
||||
})
|
||||
});
|
||||
|
||||
console.log('Update response status:', response.status);
|
||||
console.log('Update response ok:', response.ok);
|
||||
console.log('Update response headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
// Handle 504 Gateway Timeout as successful initiation
|
||||
if (response.status === 504) {
|
||||
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
|
||||
|
||||
// Update progress to show that we're now polling
|
||||
const progressBar = document.getElementById('launchProgress');
|
||||
const progressText = document.getElementById('stepDescription');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '25%';
|
||||
progressBar.textContent = '25%';
|
||||
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||
}
|
||||
|
||||
// Start polling immediately since the stack update was initiated
|
||||
console.log('Starting to poll for stack status after 504 timeout...');
|
||||
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||
return pollResult;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to update stack';
|
||||
console.log('Response not ok, status:', response.status);
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
console.log('Parsed error data:', errorData);
|
||||
} catch (parseError) {
|
||||
console.log('Failed to parse JSON error, trying text:', parseError);
|
||||
// If JSON parsing fails, try to get text content
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
console.log('Error text content:', errorText);
|
||||
if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) {
|
||||
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
|
||||
|
||||
// Update progress to show that we're now polling
|
||||
const progressBar = document.getElementById('launchProgress');
|
||||
const progressText = document.getElementById('stepDescription');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '25%';
|
||||
progressBar.textContent = '25%';
|
||||
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||
}
|
||||
|
||||
// Start polling immediately since the stack update was initiated
|
||||
console.log('Starting to poll for stack status after 504 timeout...');
|
||||
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||
return pollResult;
|
||||
} else {
|
||||
errorMessage = `HTTP ${response.status}: ${errorText}`;
|
||||
}
|
||||
} catch (textError) {
|
||||
console.log('Failed to get error text:', textError);
|
||||
errorMessage = `HTTP ${response.status}: Failed to parse response`;
|
||||
}
|
||||
}
|
||||
console.log('Throwing error:', errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Stack update initiated:', result);
|
||||
|
||||
// If stack is being updated, poll for status
|
||||
if (result.data.status === 'updating') {
|
||||
console.log('Stack is being updated, polling for status...');
|
||||
const pollResult = await pollStackStatus(stackId, 10 * 60 * 1000); // 10 minutes max
|
||||
return pollResult;
|
||||
}
|
||||
|
||||
// Return success result with response data
|
||||
return {
|
||||
success: true,
|
||||
data: result.data
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating stack:', error);
|
||||
|
||||
// Check if this is a 504 timeout error that should be handled as a success
|
||||
if (error.message && (
|
||||
error.message.includes('504 Gateway Time-out') ||
|
||||
error.message.includes('504 Gateway Timeout') ||
|
||||
error.message.includes('timed out')
|
||||
)) {
|
||||
console.log('Detected 504 timeout in catch block - treating as successful initiation');
|
||||
|
||||
// Update progress to show that we're now polling
|
||||
const progressBar = document.getElementById('launchProgress');
|
||||
const progressText = document.getElementById('stepDescription');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '25%';
|
||||
progressBar.textContent = '25%';
|
||||
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||
}
|
||||
|
||||
// Start polling immediately since the stack update was initiated
|
||||
console.log('Starting to poll for stack status after 504 timeout from catch block...');
|
||||
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||
return pollResult;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateInstance(instanceUrl, instanceId) {
|
||||
try {
|
||||
// First check if instance is already authenticated
|
||||
const instancesResponse = await fetch('/instances');
|
||||
const text = await instancesResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, 'text/html');
|
||||
|
||||
// Find the instance with matching URL
|
||||
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
|
||||
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
|
||||
return urlCell && urlCell.textContent.trim() === instanceUrl;
|
||||
});
|
||||
|
||||
if (!instanceRow) {
|
||||
throw new Error('Instance not found in database');
|
||||
}
|
||||
|
||||
// Get the instance ID from the status badge's data attribute
|
||||
const statusBadge = instanceRow.querySelector('[data-instance-id]');
|
||||
if (!statusBadge) {
|
||||
throw new Error('Could not find instance ID');
|
||||
}
|
||||
|
||||
const dbInstanceId = statusBadge.dataset.instanceId;
|
||||
|
||||
// Check if already authenticated
|
||||
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
|
||||
if (!authStatusResponse.ok) {
|
||||
throw new Error('Failed to check authentication status');
|
||||
}
|
||||
|
||||
const authStatus = await authStatusResponse.json();
|
||||
if (authStatus.authenticated) {
|
||||
console.log('Instance is already authenticated');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Instance is already authenticated',
|
||||
alreadyAuthenticated: true
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
|
||||
|
||||
// First login to get token
|
||||
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'administrator@docupulse.com',
|
||||
password: 'changeme'
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
const errorText = await loginResponse.text();
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
if (loginData.status !== 'success' || !loginData.token) {
|
||||
throw new Error('Login failed: Invalid response from server');
|
||||
}
|
||||
|
||||
const token = loginData.token;
|
||||
|
||||
// Then create management API key
|
||||
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `Connection from ${window.location.hostname}`
|
||||
})
|
||||
});
|
||||
|
||||
if (!keyResponse.ok) {
|
||||
const errorText = await keyResponse.text();
|
||||
throw new Error(`Failed to create API key: ${errorText}`);
|
||||
}
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
if (!keyData.api_key) {
|
||||
throw new Error('No API key received from server');
|
||||
}
|
||||
|
||||
// Save the token to our database
|
||||
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ token: keyData.api_key })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to save token: ${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully authenticated instance',
|
||||
alreadyAuthenticated: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user