improved launch process using cloudflare
This commit is contained in:
Binary file not shown.
@@ -848,6 +848,125 @@ def deploy_stack():
|
||||
current_app.logger.error(f"Error deploying stack: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@launch_api.route('/check-stack-status', methods=['POST'])
|
||||
@csrf.exempt
|
||||
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
|
||||
|
||||
# Get Portainer settings
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
if not portainer_settings:
|
||||
return jsonify({'error': 'Portainer settings not configured'}), 400
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
if not endpoint_response.ok:
|
||||
return jsonify({'error': 'Failed to get Portainer endpoints'}), 500
|
||||
|
||||
endpoints = endpoint_response.json()
|
||||
if not endpoints:
|
||||
return jsonify({'error': 'No Portainer endpoints found'}), 400
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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"
|
||||
services_response = requests.get(
|
||||
services_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not services_response.ok:
|
||||
return jsonify({'error': 'Failed to get stack services'}), 500
|
||||
|
||||
services = services_response.json()
|
||||
|
||||
# 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 = {
|
||||
'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
|
||||
|
||||
# Determine overall stack status
|
||||
if all_running and len(services) > 0:
|
||||
status = 'active'
|
||||
elif len(services) > 0:
|
||||
status = 'partial'
|
||||
else:
|
||||
status = 'inactive'
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'stack_name': data['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'])
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error checking stack status: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@launch_api.route('/save-instance', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def save_instance():
|
||||
@@ -1559,18 +1678,26 @@ def copy_smtp_settings():
|
||||
if not jwt_token:
|
||||
return jsonify({'error': 'No JWT token received'}), 400
|
||||
|
||||
# Prepare SMTP settings data for the API
|
||||
api_smtp_data = {
|
||||
'smtp_host': smtp_settings.get('smtp_host'),
|
||||
'smtp_port': smtp_settings.get('smtp_port'),
|
||||
'smtp_username': smtp_settings.get('smtp_username'),
|
||||
'smtp_password': smtp_settings.get('smtp_password'),
|
||||
'smtp_security': smtp_settings.get('smtp_security'),
|
||||
'smtp_from_email': smtp_settings.get('smtp_from_email'),
|
||||
'smtp_from_name': smtp_settings.get('smtp_from_name')
|
||||
}
|
||||
|
||||
# Copy SMTP settings to the launched instance
|
||||
smtp_response = requests.post(
|
||||
f"{instance_url.rstrip('/')}/api/admin/key-value",
|
||||
smtp_response = requests.put(
|
||||
f"{instance_url.rstrip('/')}/api/admin/settings",
|
||||
headers={
|
||||
'Authorization': f'Bearer {jwt_token}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'key': 'smtp_settings',
|
||||
'value': smtp_settings
|
||||
},
|
||||
json=api_smtp_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -1582,7 +1709,7 @@ def copy_smtp_settings():
|
||||
|
||||
return jsonify({
|
||||
'message': 'SMTP settings copied successfully',
|
||||
'data': smtp_settings
|
||||
'data': api_smtp_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
||||
221
routes/main.py
221
routes/main.py
@@ -391,12 +391,14 @@ def init_routes(main_bp):
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||
git_settings = KeyValueSettings.get_value('git_settings')
|
||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||
|
||||
return render_template('main/instances.html',
|
||||
instances=instances,
|
||||
portainer_settings=portainer_settings,
|
||||
nginx_settings=nginx_settings,
|
||||
git_settings=git_settings)
|
||||
git_settings=git_settings,
|
||||
cloudflare_settings=cloudflare_settings)
|
||||
|
||||
@main_bp.route('/instances/add', methods=['POST'])
|
||||
@login_required
|
||||
@@ -950,6 +952,7 @@ def init_routes(main_bp):
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||
git_settings = KeyValueSettings.get_value('git_settings')
|
||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||
|
||||
# Get management API key for the connections tab
|
||||
management_api_key = ManagementAPIKey.query.filter_by(is_active=True).first()
|
||||
@@ -1020,6 +1023,7 @@ def init_routes(main_bp):
|
||||
portainer_settings=portainer_settings,
|
||||
nginx_settings=nginx_settings,
|
||||
git_settings=git_settings,
|
||||
cloudflare_settings=cloudflare_settings,
|
||||
csrf_token=generate_csrf())
|
||||
|
||||
@main_bp.route('/settings/update-smtp', methods=['POST'])
|
||||
@@ -1678,6 +1682,77 @@ def init_routes(main_bp):
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
|
||||
|
||||
@main_bp.route('/settings/save-cloudflare-connection', methods=['POST'])
|
||||
@login_required
|
||||
def save_cloudflare_connection():
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
api_key = data.get('api_key')
|
||||
zone_id = data.get('zone_id')
|
||||
server_ip = data.get('server_ip')
|
||||
|
||||
if not email or not api_key or not zone_id or not server_ip:
|
||||
return jsonify({'error': 'Missing required fields'}), 400
|
||||
|
||||
try:
|
||||
# Save Cloudflare settings
|
||||
KeyValueSettings.set_value('cloudflare_settings', {
|
||||
'email': email,
|
||||
'api_key': api_key,
|
||||
'zone_id': zone_id,
|
||||
'server_ip': server_ip
|
||||
})
|
||||
|
||||
return jsonify({'message': 'Settings saved successfully'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@main_bp.route('/settings/test-cloudflare-connection', methods=['POST'])
|
||||
@login_required
|
||||
def test_cloudflare_connection():
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
api_key = data.get('api_key')
|
||||
zone_id = data.get('zone_id')
|
||||
server_ip = data.get('server_ip')
|
||||
|
||||
if not email or not api_key or not zone_id or not server_ip:
|
||||
return jsonify({'error': 'Missing required fields'}), 400
|
||||
|
||||
try:
|
||||
# Test Cloudflare connection by getting zone details
|
||||
headers = {
|
||||
'X-Auth-Email': email,
|
||||
'X-Auth-Key': api_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# Try to get zone information
|
||||
response = requests.get(
|
||||
f'https://api.cloudflare.com/client/v4/zones/{zone_id}',
|
||||
headers=headers,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
zone_data = response.json()
|
||||
if zone_data.get('success'):
|
||||
return jsonify({'message': 'Connection successful'})
|
||||
else:
|
||||
return jsonify({'error': f'API error: {zone_data.get("errors", [{}])[0].get("message", "Unknown error")}'}), 400
|
||||
else:
|
||||
return jsonify({'error': f'Connection failed: HTTP {response.status_code}'}), 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
|
||||
|
||||
@main_bp.route('/instances/launch-progress')
|
||||
@login_required
|
||||
@require_password_change
|
||||
@@ -1690,10 +1765,13 @@ def init_routes(main_bp):
|
||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||
# Get Portainer settings
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
# Get Cloudflare settings
|
||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||
|
||||
return render_template('main/launch_progress.html',
|
||||
nginx_settings=nginx_settings,
|
||||
portainer_settings=portainer_settings)
|
||||
portainer_settings=portainer_settings,
|
||||
cloudflare_settings=cloudflare_settings)
|
||||
|
||||
@main_bp.route('/api/check-dns', methods=['POST'])
|
||||
@login_required
|
||||
@@ -1728,6 +1806,145 @@ def init_routes(main_bp):
|
||||
'results': results
|
||||
})
|
||||
|
||||
@main_bp.route('/api/check-cloudflare-connection', methods=['POST'])
|
||||
@login_required
|
||||
@require_password_change
|
||||
def check_cloudflare_connection():
|
||||
if not os.environ.get('MASTER', 'false').lower() == 'true':
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
# Get Cloudflare settings
|
||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||
if not cloudflare_settings:
|
||||
return jsonify({'error': 'Cloudflare settings not configured'}), 400
|
||||
|
||||
try:
|
||||
# Test Cloudflare connection by getting zone details
|
||||
headers = {
|
||||
'X-Auth-Email': cloudflare_settings['email'],
|
||||
'X-Auth-Key': cloudflare_settings['api_key'],
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# Try to get zone information
|
||||
response = requests.get(
|
||||
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}',
|
||||
headers=headers,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
zone_data = response.json()
|
||||
if zone_data.get('success'):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Cloudflare connection successful',
|
||||
'zone_name': zone_data['result']['name']
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': f'API error: {zone_data.get("errors", [{}])[0].get("message", "Unknown error")}'}), 400
|
||||
else:
|
||||
return jsonify({'error': f'Connection failed: HTTP {response.status_code}'}), 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
|
||||
|
||||
@main_bp.route('/api/create-dns-records', methods=['POST'])
|
||||
@login_required
|
||||
@require_password_change
|
||||
def create_dns_records():
|
||||
if not os.environ.get('MASTER', 'false').lower() == 'true':
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'domains' not in data:
|
||||
return jsonify({'error': 'No domains provided'}), 400
|
||||
|
||||
domains = data['domains']
|
||||
|
||||
# Get Cloudflare settings
|
||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||
if not cloudflare_settings:
|
||||
return jsonify({'error': 'Cloudflare settings not configured'}), 400
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'X-Auth-Email': cloudflare_settings['email'],
|
||||
'X-Auth-Key': cloudflare_settings['api_key'],
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
results = {}
|
||||
for domain in domains:
|
||||
# Check if DNS record already exists
|
||||
response = requests.get(
|
||||
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records',
|
||||
headers=headers,
|
||||
params={'name': domain},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
dns_data = response.json()
|
||||
existing_records = dns_data.get('result', [])
|
||||
|
||||
# Filter for A records
|
||||
a_records = [record for record in existing_records if record['type'] == 'A' and record['name'] == domain]
|
||||
|
||||
if a_records:
|
||||
# Update existing A record
|
||||
record_id = a_records[0]['id']
|
||||
update_data = {
|
||||
'type': 'A',
|
||||
'name': domain,
|
||||
'content': cloudflare_settings['server_ip'],
|
||||
'ttl': 1, # Auto TTL
|
||||
'proxied': True
|
||||
}
|
||||
|
||||
update_response = requests.put(
|
||||
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records/{record_id}',
|
||||
headers=headers,
|
||||
json=update_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if update_response.status_code == 200:
|
||||
results[domain] = {'status': 'updated', 'message': 'DNS record updated'}
|
||||
else:
|
||||
results[domain] = {'status': 'error', 'message': f'Failed to update DNS record: {update_response.status_code}'}
|
||||
else:
|
||||
# Create new A record
|
||||
create_data = {
|
||||
'type': 'A',
|
||||
'name': domain,
|
||||
'content': cloudflare_settings['server_ip'],
|
||||
'ttl': 1, # Auto TTL
|
||||
'proxied': True
|
||||
}
|
||||
|
||||
create_response = requests.post(
|
||||
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records',
|
||||
headers=headers,
|
||||
json=create_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if create_response.status_code == 200:
|
||||
results[domain] = {'status': 'created', 'message': 'DNS record created'}
|
||||
else:
|
||||
results[domain] = {'status': 'error', 'message': f'Failed to create DNS record: {create_response.status_code}'}
|
||||
else:
|
||||
results[domain] = {'status': 'error', 'message': f'Failed to check existing records: {response.status_code}'}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'DNS operation failed: {str(e)}'}), 400
|
||||
|
||||
@main_bp.route('/api/mails/<int:mail_id>')
|
||||
@login_required
|
||||
def get_mail_details(mail_id):
|
||||
|
||||
@@ -16,6 +16,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function initializeSteps() {
|
||||
const stepsContainer = document.getElementById('stepsContainer');
|
||||
|
||||
// Add Cloudflare connection check step
|
||||
const cloudflareStep = document.createElement('div');
|
||||
cloudflareStep.className = 'step-item';
|
||||
cloudflareStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-cloud"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Checking Cloudflare Connection</h5>
|
||||
<p class="step-status">Verifying Cloudflare API connection...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(cloudflareStep);
|
||||
|
||||
// Add DNS record creation step
|
||||
const dnsCreateStep = document.createElement('div');
|
||||
dnsCreateStep.className = 'step-item';
|
||||
dnsCreateStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-plus-circle"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Creating DNS Records</h5>
|
||||
<p class="step-status">Setting up domain DNS records in Cloudflare...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(dnsCreateStep);
|
||||
|
||||
// Add DNS check step
|
||||
const dnsStep = document.createElement('div');
|
||||
dnsStep.className = 'step-item';
|
||||
@@ -199,8 +223,72 @@ function initializeSteps() {
|
||||
|
||||
async function startLaunch(data) {
|
||||
try {
|
||||
// Step 1: Check DNS records
|
||||
await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...');
|
||||
// Step 1: Check Cloudflare connection
|
||||
await updateStep(1, 'Checking Cloudflare Connection', 'Verifying Cloudflare API connection...');
|
||||
const cloudflareResult = await checkCloudflareConnection();
|
||||
|
||||
if (!cloudflareResult.success) {
|
||||
throw new Error(cloudflareResult.error || 'Failed to connect to Cloudflare');
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const cloudflareStep = document.querySelectorAll('.step-item')[0];
|
||||
cloudflareStep.classList.remove('active');
|
||||
cloudflareStep.classList.add('completed');
|
||||
cloudflareStep.querySelector('.step-status').textContent = `Successfully connected to Cloudflare (${cloudflareResult.zone_name})`;
|
||||
|
||||
// Step 2: Create DNS records
|
||||
await updateStep(2, 'Creating DNS Records', 'Setting up domain DNS records in Cloudflare...');
|
||||
const dnsCreateResult = await createDNSRecords(data.webAddresses);
|
||||
|
||||
if (!dnsCreateResult.success) {
|
||||
throw new Error(dnsCreateResult.error || 'Failed to create DNS records');
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const dnsCreateStep = document.querySelectorAll('.step-item')[1];
|
||||
dnsCreateStep.classList.remove('active');
|
||||
dnsCreateStep.classList.add('completed');
|
||||
dnsCreateStep.querySelector('.step-status').textContent = 'DNS records created successfully';
|
||||
|
||||
// Add DNS creation details
|
||||
const dnsCreateDetails = document.createElement('div');
|
||||
dnsCreateDetails.className = 'mt-3';
|
||||
dnsCreateDetails.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">DNS Record Creation Results</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Status</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.entries(dnsCreateResult.results).map(([domain, result]) => `
|
||||
<tr>
|
||||
<td>${domain}</td>
|
||||
<td>
|
||||
<span class="badge bg-${result.status === 'created' || result.status === 'updated' ? 'success' : 'danger'}">
|
||||
${result.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>${result.message}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
dnsCreateStep.querySelector('.step-content').appendChild(dnsCreateDetails);
|
||||
|
||||
// Step 3: Check DNS records
|
||||
await updateStep(3, 'Checking DNS Records', 'Verifying domain configurations...');
|
||||
const dnsResult = await checkDNSRecords(data.webAddresses);
|
||||
|
||||
// Check if any domains failed to resolve
|
||||
@@ -213,7 +301,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const dnsStep = document.querySelectorAll('.step-item')[0];
|
||||
const dnsStep = document.querySelectorAll('.step-item')[2];
|
||||
dnsStep.classList.remove('active');
|
||||
dnsStep.classList.add('completed');
|
||||
|
||||
@@ -259,8 +347,8 @@ async function startLaunch(data) {
|
||||
statusText.textContent = 'DNS records verified successfully';
|
||||
statusText.after(detailsSection);
|
||||
|
||||
// Step 2: Check NGINX connection
|
||||
await updateStep(2, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...');
|
||||
// Step 4: Check NGINX connection
|
||||
await updateStep(4, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...');
|
||||
const nginxResult = await checkNginxConnection();
|
||||
|
||||
if (!nginxResult.success) {
|
||||
@@ -268,29 +356,29 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const nginxStep = document.querySelectorAll('.step-item')[1];
|
||||
const nginxStep = document.querySelectorAll('.step-item')[3];
|
||||
nginxStep.classList.remove('active');
|
||||
nginxStep.classList.add('completed');
|
||||
nginxStep.querySelector('.step-status').textContent = 'Successfully connected to NGINX Proxy Manager';
|
||||
|
||||
// Step 3: Generate SSL Certificate
|
||||
await updateStep(3, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...');
|
||||
// Step 5: Generate SSL Certificate
|
||||
await updateStep(5, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...');
|
||||
const sslResult = await generateSSLCertificate(data.webAddresses);
|
||||
|
||||
if (!sslResult.success) {
|
||||
throw new Error(sslResult.error || 'Failed to generate SSL certificate');
|
||||
}
|
||||
|
||||
// Step 4: Create Proxy Host
|
||||
await updateStep(4, 'Creating Proxy Host', 'Setting up NGINX proxy host configuration...');
|
||||
// Step 6: Create Proxy Host
|
||||
await updateStep(6, 'Creating Proxy Host', 'Setting up NGINX proxy host configuration...');
|
||||
const proxyResult = await createProxyHost(data.webAddresses, data.port, sslResult.data.certificate.id);
|
||||
|
||||
if (!proxyResult.success) {
|
||||
throw new Error(proxyResult.error || 'Failed to create proxy host');
|
||||
}
|
||||
|
||||
// Step 5: Check Portainer connection
|
||||
await updateStep(5, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
|
||||
// Step 7: Check Portainer connection
|
||||
await updateStep(7, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
|
||||
const portainerResult = await checkPortainerConnection();
|
||||
|
||||
if (!portainerResult.success) {
|
||||
@@ -298,13 +386,13 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const portainerStep = document.querySelectorAll('.step-item')[4];
|
||||
const portainerStep = document.querySelectorAll('.step-item')[6];
|
||||
portainerStep.classList.remove('active');
|
||||
portainerStep.classList.add('completed');
|
||||
portainerStep.querySelector('.step-status').textContent = portainerResult.message;
|
||||
|
||||
// Step 6: Download Docker Compose
|
||||
await updateStep(6, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
|
||||
// Step 8: Download Docker Compose
|
||||
await updateStep(8, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
|
||||
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
|
||||
|
||||
if (!dockerComposeResult.success) {
|
||||
@@ -312,7 +400,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const dockerComposeStep = document.querySelectorAll('.step-item')[5];
|
||||
const dockerComposeStep = document.querySelectorAll('.step-item')[7];
|
||||
dockerComposeStep.classList.remove('active');
|
||||
dockerComposeStep.classList.add('completed');
|
||||
dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml';
|
||||
@@ -334,8 +422,8 @@ async function startLaunch(data) {
|
||||
};
|
||||
dockerComposeStep.querySelector('.step-content').appendChild(downloadButton);
|
||||
|
||||
// Step 7: Deploy Stack
|
||||
await updateStep(7, 'Deploying Stack', 'Launching your application stack...');
|
||||
// Step 9: Deploy Stack
|
||||
await updateStep(9, 'Deploying Stack', 'Launching your application stack...');
|
||||
const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port);
|
||||
|
||||
if (!stackResult.success) {
|
||||
@@ -343,7 +431,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const stackDeployStep = document.querySelectorAll('.step-item')[6];
|
||||
const stackDeployStep = document.querySelectorAll('.step-item')[8];
|
||||
stackDeployStep.classList.remove('active');
|
||||
stackDeployStep.classList.add('completed');
|
||||
stackDeployStep.querySelector('.step-status').textContent =
|
||||
@@ -392,7 +480,7 @@ async function startLaunch(data) {
|
||||
stackDeployStep.querySelector('.step-content').appendChild(stackDetails);
|
||||
|
||||
// Save instance data
|
||||
await updateStep(8, 'Saving Instance Data', 'Storing instance information...');
|
||||
await updateStep(10, 'Saving Instance Data', 'Storing instance information...');
|
||||
try {
|
||||
const instanceData = {
|
||||
name: data.instanceName,
|
||||
@@ -407,15 +495,15 @@ async function startLaunch(data) {
|
||||
console.log('Saving instance data:', instanceData);
|
||||
const saveResult = await saveInstanceData(instanceData);
|
||||
console.log('Save result:', saveResult);
|
||||
await updateStep(8, 'Saving Instance Data', 'Instance data saved successfully');
|
||||
await updateStep(10, 'Saving Instance Data', 'Instance data saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Error saving instance data:', error);
|
||||
await updateStep(8, 'Saving Instance Data', `Error: ${error.message}`);
|
||||
await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const saveDataStep = document.querySelectorAll('.step-item')[7];
|
||||
const saveDataStep = document.querySelectorAll('.step-item')[9];
|
||||
saveDataStep.classList.remove('active');
|
||||
saveDataStep.classList.add('completed');
|
||||
saveDataStep.querySelector('.step-status').textContent = 'Successfully saved instance data';
|
||||
@@ -453,7 +541,7 @@ async function startLaunch(data) {
|
||||
saveDataStep.querySelector('.step-content').appendChild(instanceDetails);
|
||||
|
||||
// After saving instance data, add the health check step
|
||||
await updateStep(9, 'Health Check', 'Verifying instance health...');
|
||||
await updateStep(11, 'Health Check', 'Verifying instance health...');
|
||||
const healthResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`);
|
||||
|
||||
if (!healthResult.success) {
|
||||
@@ -461,7 +549,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Add a retry button if health check fails
|
||||
const healthStep = document.querySelectorAll('.step-item')[8];
|
||||
const healthStep = document.querySelectorAll('.step-item')[10];
|
||||
if (!healthResult.success) {
|
||||
const retryButton = document.createElement('button');
|
||||
retryButton.className = 'btn btn-sm btn-warning mt-2';
|
||||
@@ -483,7 +571,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// After health check, add authentication step
|
||||
await updateStep(10, 'Instance Authentication', 'Setting up instance authentication...');
|
||||
await updateStep(12, 'Instance Authentication', 'Setting up instance authentication...');
|
||||
const authResult = await authenticateInstance(`https://${data.webAddresses[0]}`, data.instanceId);
|
||||
|
||||
if (!authResult.success) {
|
||||
@@ -491,7 +579,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the auth step to show success
|
||||
const authStep = document.querySelectorAll('.step-item')[9];
|
||||
const authStep = document.querySelectorAll('.step-item')[11];
|
||||
authStep.classList.remove('active');
|
||||
authStep.classList.add('completed');
|
||||
authStep.querySelector('.step-status').textContent = authResult.alreadyAuthenticated ?
|
||||
@@ -538,8 +626,8 @@ async function startLaunch(data) {
|
||||
`;
|
||||
authStep.querySelector('.step-content').appendChild(authDetails);
|
||||
|
||||
// Step 11: Apply Company Information
|
||||
await updateStep(11, 'Apply Company Information', 'Configuring company details...');
|
||||
// Step 13: Apply Company Information
|
||||
await updateStep(13, 'Apply Company Information', 'Configuring company details...');
|
||||
const companyResult = await applyCompanyInformation(`https://${data.webAddresses[0]}`, data.company);
|
||||
|
||||
if (!companyResult.success) {
|
||||
@@ -547,7 +635,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the company step to show success
|
||||
const companyStep = document.querySelectorAll('.step-item')[10];
|
||||
const companyStep = document.querySelectorAll('.step-item')[12];
|
||||
companyStep.classList.remove('active');
|
||||
companyStep.classList.add('completed');
|
||||
companyStep.querySelector('.step-status').textContent = 'Successfully applied company information';
|
||||
@@ -596,8 +684,8 @@ async function startLaunch(data) {
|
||||
`;
|
||||
companyStep.querySelector('.step-content').appendChild(companyDetails);
|
||||
|
||||
// Step 12: Apply Colors
|
||||
await updateStep(12, 'Apply Colors', 'Configuring color scheme...');
|
||||
// Step 14: Apply Colors
|
||||
await updateStep(14, 'Apply Colors', 'Configuring color scheme...');
|
||||
const colorsResult = await applyColors(`https://${data.webAddresses[0]}`, data.colors);
|
||||
|
||||
if (!colorsResult.success) {
|
||||
@@ -605,7 +693,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the colors step to show success
|
||||
const colorsStep = document.querySelectorAll('.step-item')[11];
|
||||
const colorsStep = document.querySelectorAll('.step-item')[13];
|
||||
colorsStep.classList.remove('active');
|
||||
colorsStep.classList.add('completed');
|
||||
colorsStep.querySelector('.step-status').textContent = 'Successfully applied color scheme';
|
||||
@@ -645,8 +733,8 @@ async function startLaunch(data) {
|
||||
`;
|
||||
colorsStep.querySelector('.step-content').appendChild(colorsDetails);
|
||||
|
||||
// Step 13: Update Admin Credentials
|
||||
await updateStep(13, 'Update Admin Credentials', 'Setting up admin account...');
|
||||
// Step 15: Update Admin Credentials
|
||||
await updateStep(15, 'Update Admin Credentials', 'Setting up admin account...');
|
||||
const credentialsResult = await updateAdminCredentials(`https://${data.webAddresses[0]}`, data.company.email);
|
||||
|
||||
if (!credentialsResult.success) {
|
||||
@@ -654,7 +742,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the credentials step to show success
|
||||
const credentialsStep = document.querySelectorAll('.step-item')[12];
|
||||
const credentialsStep = document.querySelectorAll('.step-item')[14];
|
||||
credentialsStep.classList.remove('active');
|
||||
credentialsStep.classList.add('completed');
|
||||
|
||||
@@ -719,8 +807,8 @@ async function startLaunch(data) {
|
||||
`;
|
||||
credentialsStep.querySelector('.step-content').appendChild(credentialsDetails);
|
||||
|
||||
// Step 14: Copy SMTP Settings
|
||||
await updateStep(14, 'Copy SMTP Settings', 'Configuring email settings...');
|
||||
// Step 16: Copy SMTP Settings
|
||||
await updateStep(16, 'Copy SMTP Settings', 'Configuring email settings...');
|
||||
const smtpResult = await copySmtpSettings(`https://${data.webAddresses[0]}`);
|
||||
|
||||
if (!smtpResult.success) {
|
||||
@@ -728,7 +816,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the SMTP step to show success
|
||||
const smtpStep = document.querySelectorAll('.step-item')[13];
|
||||
const smtpStep = document.querySelectorAll('.step-item')[15];
|
||||
smtpStep.classList.remove('active');
|
||||
smtpStep.classList.add('completed');
|
||||
smtpStep.querySelector('.step-status').textContent = 'Successfully copied SMTP settings';
|
||||
@@ -789,8 +877,8 @@ async function startLaunch(data) {
|
||||
`;
|
||||
smtpStep.querySelector('.step-content').appendChild(smtpDetails);
|
||||
|
||||
// Step 15: Send Completion Email
|
||||
await updateStep(15, 'Send Completion Email', 'Sending notification to client...');
|
||||
// Step 17: Send Completion Email
|
||||
await updateStep(17, 'Send Completion Email', 'Sending notification to client...');
|
||||
const emailResult = await sendCompletionEmail(`https://${data.webAddresses[0]}`, data.company, credentialsResult.data);
|
||||
|
||||
if (!emailResult.success) {
|
||||
@@ -798,7 +886,7 @@ async function startLaunch(data) {
|
||||
}
|
||||
|
||||
// Update the email step to show success
|
||||
const emailStep = document.querySelectorAll('.step-item')[14];
|
||||
const emailStep = document.querySelectorAll('.step-item')[16];
|
||||
emailStep.classList.remove('active');
|
||||
emailStep.classList.add('completed');
|
||||
emailStep.querySelector('.step-status').textContent = 'Successfully sent completion email';
|
||||
@@ -983,31 +1071,128 @@ Thank you for choosing DocuPulse!
|
||||
|
||||
} catch (error) {
|
||||
console.error('Launch failed:', error);
|
||||
await updateStep(15, 'Send Completion Email', `Error: ${error.message}`);
|
||||
await updateStep(17, 'Send Completion Email', `Error: ${error.message}`);
|
||||
showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDNSRecords(domains) {
|
||||
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
|
||||
const baseDelay = 10000; // 10 seconds base delay
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await fetch('/api/check-dns', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ domains: domains })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to check DNS records');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Check if all domains are resolved
|
||||
const allResolved = Object.values(result.results).every(result => result.resolved);
|
||||
|
||||
if (allResolved) {
|
||||
console.log(`DNS records resolved successfully on attempt ${attempt}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// If not all domains are resolved and this isn't the last attempt, wait and retry
|
||||
if (attempt < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(1.2, attempt - 1); // Exponential backoff
|
||||
const failedDomains = Object.entries(result.results)
|
||||
.filter(([_, result]) => !result.resolved)
|
||||
.map(([domain]) => domain);
|
||||
|
||||
console.log(`Attempt ${attempt}/${maxRetries}: DNS not yet propagated for ${failedDomains.join(', ')}. Waiting ${Math.round(delay/1000)}s before retry...`);
|
||||
|
||||
// Update the step description to show retry progress
|
||||
const currentStep = document.querySelector('.step-item.active');
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = `Waiting for DNS propagation... (Attempt ${attempt}/${maxRetries})`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
// Last attempt failed
|
||||
console.log(`DNS records failed to resolve after ${maxRetries} attempts`);
|
||||
return result;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error checking DNS records (attempt ${attempt}):`, error);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wait before retrying on error
|
||||
const delay = baseDelay * Math.pow(1.2, attempt - 1);
|
||||
console.log(`DNS check failed, retrying in ${Math.round(delay/1000)}s...`);
|
||||
|
||||
// Update the step description to show retry progress
|
||||
const currentStep = document.querySelector('.step-item.active');
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = `DNS check failed, retrying... (Attempt ${attempt}/${maxRetries})`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCloudflareConnection() {
|
||||
try {
|
||||
const response = await fetch('/api/check-dns', {
|
||||
const response = await fetch('/api/check-cloudflare-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ domains })
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to check DNS records');
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to check Cloudflare connection');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('DNS check result:', result);
|
||||
return result;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error checking DNS records:', error);
|
||||
console.error('Error checking Cloudflare connection:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createDNSRecords(domains) {
|
||||
try {
|
||||
const response = await fetch('/api/create-dns-records', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ domains: domains })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create DNS records');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error creating DNS records:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1390,7 +1575,7 @@ async function createProxyHost(domains, port, sslCertificateId) {
|
||||
`;
|
||||
|
||||
// Update the proxy step to show success and add the results
|
||||
const proxyStep = document.querySelectorAll('.step-item')[3];
|
||||
const proxyStep = document.querySelectorAll('.step-item')[5];
|
||||
proxyStep.classList.remove('active');
|
||||
proxyStep.classList.add('completed');
|
||||
const statusText = proxyStep.querySelector('.step-status');
|
||||
@@ -1509,7 +1694,7 @@ async function generateSSLCertificate(domains) {
|
||||
}
|
||||
|
||||
// Update the SSL step to show success
|
||||
const sslStep = document.querySelectorAll('.step-item')[2];
|
||||
const sslStep = document.querySelectorAll('.step-item')[4];
|
||||
sslStep.classList.remove('active');
|
||||
sslStep.classList.add('completed');
|
||||
const sslStatusText = sslStep.querySelector('.step-status');
|
||||
@@ -1589,8 +1774,8 @@ function updateStep(stepNumber, title, description) {
|
||||
document.getElementById('currentStep').textContent = title;
|
||||
document.getElementById('stepDescription').textContent = description;
|
||||
|
||||
// Calculate progress based on total number of steps (14 steps total)
|
||||
const totalSteps = 14;
|
||||
// Calculate progress based on total number of steps (17 steps total)
|
||||
const totalSteps = 17;
|
||||
const progress = ((stepNumber - 1) / (totalSteps - 1)) * 100;
|
||||
const progressBar = document.getElementById('launchProgress');
|
||||
progressBar.style.width = `${progress}%`;
|
||||
@@ -1674,7 +1859,11 @@ async function downloadDockerCompose(repo, branch) {
|
||||
|
||||
// Add new function to deploy stack
|
||||
async function deployStack(dockerComposeContent, stackName, port) {
|
||||
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
|
||||
const baseDelay = 10000; // 10 seconds base delay
|
||||
|
||||
try {
|
||||
// First, attempt to deploy the stack
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10 minutes timeout
|
||||
|
||||
@@ -1709,10 +1898,135 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// If deployment was successful, wait for stack to come online
|
||||
if (result.success || result.data) {
|
||||
console.log('Stack deployment initiated, waiting for stack to come online...');
|
||||
|
||||
// Update status to show we're waiting for stack to come online
|
||||
const currentStep = document.querySelector('.step-item.active');
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = 'Stack deployed, waiting for services to start...';
|
||||
}
|
||||
|
||||
// Wait and retry to check if stack is online
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Check stack status via Portainer API
|
||||
const stackCheckResponse = await fetch('/api/admin/check-stack-status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stack_name: `docupulse_${port}`
|
||||
})
|
||||
});
|
||||
|
||||
if (stackCheckResponse.ok) {
|
||||
const stackStatus = await stackCheckResponse.json();
|
||||
|
||||
if (stackStatus.success && stackStatus.data.status === 'active') {
|
||||
console.log(`Stack came online successfully on attempt ${attempt}`);
|
||||
|
||||
// Update status to show success
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = 'Stack deployed and online successfully';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result.data || result,
|
||||
status: 'active',
|
||||
attempt: attempt
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If not online yet and this isn't the last attempt, wait and retry
|
||||
if (attempt < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(1.2, attempt - 1); // Exponential backoff
|
||||
|
||||
console.log(`Attempt ${attempt}/${maxRetries}: Stack not yet online. Waiting ${Math.round(delay/1000)}s before retry...`);
|
||||
|
||||
// Update the step description to show retry progress
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = `Waiting for stack to come online... (Attempt ${attempt}/${maxRetries})`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
// Last attempt failed - stack might be online but API check failed
|
||||
console.log(`Stack status check failed after ${maxRetries} attempts, but deployment was successful`);
|
||||
|
||||
// Update status to show partial success
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = 'Stack deployed (status check timeout)';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result.data || result,
|
||||
status: 'deployed',
|
||||
note: 'Status check timeout - stack may be online'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Stack status check attempt ${attempt} failed:`, error);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
// Last attempt failed, but deployment was successful
|
||||
console.log('Stack status check failed after all attempts, but deployment was successful');
|
||||
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = 'Stack deployed (status check failed)';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result.data || result,
|
||||
status: 'deployed',
|
||||
note: 'Status check failed - stack may be online'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Wait before retrying on error
|
||||
const delay = baseDelay * Math.pow(1.2, attempt - 1);
|
||||
console.log(`Stack check failed, retrying in ${Math.round(delay/1000)}s...`);
|
||||
|
||||
if (currentStep) {
|
||||
const statusElement = currentStep.querySelector('.step-status');
|
||||
statusElement.textContent = `Stack check failed, retrying... (Attempt ${attempt}/${maxRetries})`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, deployment was successful but we couldn't verify status
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
data: {
|
||||
...result.data || result,
|
||||
status: 'deployed',
|
||||
note: 'Deployment successful, status unknown'
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deploying stack:', error);
|
||||
return {
|
||||
|
||||
@@ -435,6 +435,100 @@ async function saveGitConnection(event, provider) {
|
||||
saveModal.show();
|
||||
}
|
||||
|
||||
// Test Cloudflare Connection
|
||||
async function testCloudflareConnection() {
|
||||
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
|
||||
const messageElement = document.getElementById('saveConnectionMessage');
|
||||
messageElement.textContent = 'Testing connection...';
|
||||
messageElement.className = '';
|
||||
saveModal.show();
|
||||
|
||||
try {
|
||||
const email = document.getElementById('cloudflareEmail').value;
|
||||
const apiKey = document.getElementById('cloudflareApiKey').value;
|
||||
const zoneId = document.getElementById('cloudflareZone').value;
|
||||
const serverIp = document.getElementById('cloudflareServerIp').value;
|
||||
|
||||
if (!email || !apiKey || !zoneId || !serverIp) {
|
||||
throw new Error('Please fill in all required fields');
|
||||
}
|
||||
|
||||
const data = {
|
||||
email: email,
|
||||
api_key: apiKey,
|
||||
zone_id: zoneId,
|
||||
server_ip: serverIp
|
||||
};
|
||||
|
||||
const response = await fetch('/settings/test-cloudflare-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Connection test failed');
|
||||
}
|
||||
|
||||
messageElement.textContent = 'Connection test successful!';
|
||||
messageElement.className = 'text-success';
|
||||
} catch (error) {
|
||||
messageElement.textContent = error.message || 'Connection test failed';
|
||||
messageElement.className = 'text-danger';
|
||||
}
|
||||
}
|
||||
|
||||
// Save Cloudflare Connection
|
||||
async function saveCloudflareConnection(event) {
|
||||
event.preventDefault();
|
||||
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
|
||||
const messageElement = document.getElementById('saveConnectionMessage');
|
||||
messageElement.textContent = '';
|
||||
messageElement.className = '';
|
||||
|
||||
try {
|
||||
const email = document.getElementById('cloudflareEmail').value;
|
||||
const apiKey = document.getElementById('cloudflareApiKey').value;
|
||||
const zoneId = document.getElementById('cloudflareZone').value;
|
||||
const serverIp = document.getElementById('cloudflareServerIp').value;
|
||||
|
||||
if (!email || !apiKey || !zoneId || !serverIp) {
|
||||
throw new Error('Please fill in all required fields');
|
||||
}
|
||||
|
||||
const response = await fetch('/settings/save-cloudflare-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
api_key: apiKey,
|
||||
zone_id: zoneId,
|
||||
server_ip: serverIp
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to save settings');
|
||||
}
|
||||
|
||||
messageElement.textContent = 'Settings saved successfully!';
|
||||
messageElement.className = 'text-success';
|
||||
} catch (error) {
|
||||
messageElement.textContent = error.message || 'Failed to save settings';
|
||||
messageElement.className = 'text-danger';
|
||||
}
|
||||
|
||||
saveModal.show();
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const gitSettings = JSON.parse(document.querySelector('meta[name="git-settings"]').getAttribute('content'));
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<p class="mb-1"><a href="{{ url_for('auth.forgot_password') }}" class="text-decoration-none">Forgot your password?</a></p>
|
||||
<p class="mb-0">Don't have an account? <a href="{{ url_for('auth.register') }}" class="text-decoration-none">Sign Up</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
api_key: '{{ portainer_settings.api_key if portainer_settings else "" }}'
|
||||
};
|
||||
|
||||
window.cloudflareSettings = {
|
||||
email: '{{ cloudflare_settings.email if cloudflare_settings else "" }}',
|
||||
api_key: '{{ cloudflare_settings.api_key if cloudflare_settings else "" }}',
|
||||
zone_id: '{{ cloudflare_settings.zone_id if cloudflare_settings else "" }}',
|
||||
server_ip: '{{ cloudflare_settings.server_ip if cloudflare_settings else "" }}'
|
||||
};
|
||||
|
||||
// Pass CSRF token to JavaScript
|
||||
window.csrfToken = '{{ csrf_token }}';
|
||||
</script>
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
{% if is_master %}
|
||||
<!-- Connections Tab -->
|
||||
<div class="tab-pane fade {% if active_tab == 'connections' %}show active{% endif %}" id="connections" role="tabpanel" aria-labelledby="connections-tab">
|
||||
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) }}
|
||||
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% from "settings/components/connection_modals.html" import connection_modals %}
|
||||
|
||||
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %}
|
||||
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) %}
|
||||
<!-- Meta tags for JavaScript -->
|
||||
<meta name="management-api-key" content="{{ site_settings.management_api_key }}">
|
||||
<meta name="git-settings" content="{{ git_settings|tojson|safe }}">
|
||||
@@ -161,6 +161,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Connection Card -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-cloud me-2"></i>Cloudflare Connection
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="testCloudflareConnection()">
|
||||
<i class="fas fa-plug me-1"></i>Test Connection
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="cloudflareForm" onsubmit="saveCloudflareConnection(event)">
|
||||
<div class="mb-3">
|
||||
<label for="cloudflareEmail" class="form-label">Email Address</label>
|
||||
<input type="email" class="form-control" id="cloudflareEmail" name="cloudflareEmail"
|
||||
placeholder="Enter your Cloudflare email" required
|
||||
value="{{ cloudflare_settings.email if cloudflare_settings and cloudflare_settings.email else '' }}">
|
||||
<div class="form-text">The email address associated with your Cloudflare account</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cloudflareApiKey" class="form-label">API Key</label>
|
||||
<input type="password" class="form-control" id="cloudflareApiKey" name="cloudflareApiKey"
|
||||
placeholder="Enter your Cloudflare API key" required
|
||||
value="{{ cloudflare_settings.api_key if cloudflare_settings and cloudflare_settings.api_key else '' }}">
|
||||
<div class="form-text">You can generate this in your Cloudflare account settings</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cloudflareZone" class="form-label">Zone ID</label>
|
||||
<input type="text" class="form-control" id="cloudflareZone" name="cloudflareZone"
|
||||
placeholder="Enter your Cloudflare zone ID" required
|
||||
value="{{ cloudflare_settings.zone_id if cloudflare_settings and cloudflare_settings.zone_id else '' }}">
|
||||
<div class="form-text">The zone ID for your domain in Cloudflare</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cloudflareServerIp" class="form-label">Server IP Address</label>
|
||||
<input type="text" class="form-control" id="cloudflareServerIp" name="cloudflareServerIp"
|
||||
placeholder="Enter your server IP address (e.g., 192.168.1.100)" required
|
||||
value="{{ cloudflare_settings.server_ip if cloudflare_settings and cloudflare_settings.server_ip else '' }}">
|
||||
<div class="form-text">The IP address of this server for DNS management</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Cloudflare Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Connection Modal -->
|
||||
|
||||
Reference in New Issue
Block a user