document.addEventListener('DOMContentLoaded', function() { // Get the launch data from sessionStorage const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); if (!launchData) { showError('No launch data found. Please start over.'); return; } // Initialize the steps initializeSteps(); // Start the launch process startLaunch(launchData); }); function initializeSteps() { const stepsContainer = document.getElementById('stepsContainer'); // Add DNS check step const dnsStep = document.createElement('div'); dnsStep.className = 'step-item'; dnsStep.innerHTML = `
Checking DNS Records

Verifying domain configurations...

`; stepsContainer.appendChild(dnsStep); // Add NGINX connection check step const nginxStep = document.createElement('div'); nginxStep.className = 'step-item'; nginxStep.innerHTML = `
Checking NGINX Connection

Verifying connection to NGINX Proxy Manager...

`; stepsContainer.appendChild(nginxStep); // Add SSL Certificate generation step const sslStep = document.createElement('div'); sslStep.className = 'step-item'; sslStep.innerHTML = `
Generating SSL Certificate

Setting up secure HTTPS connection...

`; stepsContainer.appendChild(sslStep); // Add Proxy Host creation step const proxyStep = document.createElement('div'); proxyStep.className = 'step-item'; proxyStep.innerHTML = `
Creating Proxy Host

Setting up NGINX proxy host configuration...

`; stepsContainer.appendChild(proxyStep); // Add Portainer connection check step const portainerStep = document.createElement('div'); portainerStep.className = 'step-item'; portainerStep.innerHTML = `
Checking Portainer Connection

Verifying connection to Portainer...

`; stepsContainer.appendChild(portainerStep); // Add Docker Compose download step const dockerComposeStep = document.createElement('div'); dockerComposeStep.className = 'step-item'; dockerComposeStep.innerHTML = `
Downloading Docker Compose

Fetching docker-compose.yml from repository...

`; stepsContainer.appendChild(dockerComposeStep); // Add Portainer stack deployment step const stackDeployStep = document.createElement('div'); stackDeployStep.className = 'step-item'; stackDeployStep.innerHTML = `
Deploying Stack

Launching your application stack...

`; stepsContainer.appendChild(stackDeployStep); // Add Save Instance Data step const saveDataStep = document.createElement('div'); saveDataStep.className = 'step-item'; saveDataStep.innerHTML = `
Saving Instance Data

Storing instance information...

`; stepsContainer.appendChild(saveDataStep); // Add Health Check step const healthStep = document.createElement('div'); healthStep.className = 'step-item'; healthStep.innerHTML = `
Health Check

Verifying instance health...

`; stepsContainer.appendChild(healthStep); } async function startLaunch(data) { try { // Step 1: Check DNS records await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...'); const dnsResult = await checkDNSRecords(data.webAddresses); // Check if any domains failed to resolve const failedDomains = Object.entries(dnsResult.results) .filter(([_, result]) => !result.resolved) .map(([domain]) => domain); if (failedDomains.length > 0) { throw new Error(`DNS records not found for: ${failedDomains.join(', ')}`); } // Update the step to show success const dnsStep = document.querySelectorAll('.step-item')[0]; dnsStep.classList.remove('active'); dnsStep.classList.add('completed'); // Create a details section for DNS results const detailsSection = document.createElement('div'); detailsSection.className = 'mt-3'; detailsSection.innerHTML = `
DNS Check Results
${Object.entries(dnsResult.results).map(([domain, result]) => ` `).join('')}
Domain Status IP Address TTL
${domain} ${result.resolved ? 'Resolved' : 'Not Found'} ${result.ip || 'N/A'} ${result.ttl || 'N/A'}
`; // Add the details section after the status text const statusText = dnsStep.querySelector('.step-status'); 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...'); const nginxResult = await checkNginxConnection(); if (!nginxResult.success) { throw new Error(nginxResult.error || 'Failed to connect to NGINX Proxy Manager'); } // Update the step to show success const nginxStep = document.querySelectorAll('.step-item')[1]; 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...'); 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...'); 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...'); const portainerResult = await checkPortainerConnection(); if (!portainerResult.success) { throw new Error(portainerResult.message || 'Failed to connect to Portainer'); } // Update the step to show success const portainerStep = document.querySelectorAll('.step-item')[4]; 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...'); const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch); if (!dockerComposeResult.success) { throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml'); } // Update the step to show success const dockerComposeStep = document.querySelectorAll('.step-item')[5]; dockerComposeStep.classList.remove('active'); dockerComposeStep.classList.add('completed'); dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml'; // Add download button const downloadButton = document.createElement('button'); downloadButton.className = 'btn btn-sm btn-primary mt-2'; downloadButton.innerHTML = ' Download docker-compose.yml'; downloadButton.onclick = () => { const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'docker-compose.yml'; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }; dockerComposeStep.querySelector('.step-content').appendChild(downloadButton); // Step 7: Deploy Stack await updateStep(7, 'Deploying Stack', 'Launching your application stack...'); const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port); if (!stackResult.success) { throw new Error(stackResult.error || 'Failed to deploy stack'); } // Update the step to show success const stackDeployStep = document.querySelectorAll('.step-item')[6]; stackDeployStep.classList.remove('active'); stackDeployStep.classList.add('completed'); stackDeployStep.querySelector('.step-status').textContent = stackResult.data.status === 'existing' ? 'Using existing stack' : 'Successfully deployed stack'; // Add stack details const stackDetails = document.createElement('div'); stackDetails.className = 'mt-3'; stackDetails.innerHTML = `
Stack Deployment Results
Property Value
Stack Name ${stackResult.data.name}
Stack ID ${stackResult.data.id}
Status ${stackResult.data.status === 'existing' ? 'Existing' : 'Deployed'}
`; stackDeployStep.querySelector('.step-content').appendChild(stackDetails); // Save instance data await updateStep(8, 'Saving Instance Data', 'Storing instance information...'); try { const instanceData = { name: data.instanceName, port: data.port, domains: data.webAddresses, stack_id: stackResult.data.id, stack_name: stackResult.data.name, status: stackResult.data.status, repository: data.repository, branch: data.branch }; 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'); } catch (error) { console.error('Error saving instance data:', error); await updateStep(8, 'Saving Instance Data', `Error: ${error.message}`); throw error; } // Update the step to show success const saveDataStep = document.querySelectorAll('.step-item')[7]; saveDataStep.classList.remove('active'); saveDataStep.classList.add('completed'); saveDataStep.querySelector('.step-status').textContent = 'Successfully saved instance data'; // Add instance details const instanceDetails = document.createElement('div'); instanceDetails.className = 'mt-3'; instanceDetails.innerHTML = `
Instance Information
Property Value
Internal Port ${data.port}
Domains ${data.webAddresses.join(', ')}
`; saveDataStep.querySelector('.step-content').appendChild(instanceDetails); // After saving instance data, add the health check step await updateStep(9, 'Health Check', 'Verifying instance health...'); const healthResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`); if (!healthResult.success) { throw new Error(`Health check failed: ${healthResult.error}`); } // Add a retry button if health check fails const healthStep = document.querySelectorAll('.step-item')[8]; if (!healthResult.success) { const retryButton = document.createElement('button'); retryButton.className = 'btn btn-sm btn-warning mt-2'; retryButton.innerHTML = ' Retry Health Check'; retryButton.onclick = async () => { retryButton.disabled = true; retryButton.innerHTML = ' Checking...'; const retryResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`); if (retryResult.success) { healthStep.classList.remove('failed'); healthStep.classList.add('completed'); retryButton.remove(); } else { retryButton.disabled = false; retryButton.innerHTML = ' Retry Health Check'; } }; healthStep.querySelector('.step-content').appendChild(retryButton); } } catch (error) { console.error('Launch failed:', error); await updateStep(9, 'Health Check', `Error: ${error.message}`); showError(error.message); } } async function checkDNSRecords(domains) { try { const response = await fetch('/api/check-dns', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ domains }) }); if (!response.ok) { throw new Error('Failed to check DNS records'); } const result = await response.json(); console.log('DNS check result:', result); return result; } catch (error) { console.error('Error checking DNS records:', error); throw error; } } async function checkNginxConnection() { try { // Get NGINX settings from the template const nginxSettings = { url: window.nginxSettings?.url || '', username: window.nginxSettings?.username || '', password: window.nginxSettings?.password || '' }; // Debug log the settings (without password) console.log('NGINX Settings:', { url: nginxSettings.url, username: nginxSettings.username, hasPassword: !!nginxSettings.password }); // Check if any required field is missing if (!nginxSettings.url || !nginxSettings.username || !nginxSettings.password) { return { success: false, error: 'NGINX settings are not configured. Please configure NGINX settings in the admin panel.' }; } // First, get the token const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: nginxSettings.username, secret: nginxSettings.password }) }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error('Token Error Response:', errorText); try { const errorJson = JSON.parse(errorText); throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`); } catch (e) { throw new Error(`Failed to authenticate with NGINX: ${errorText}`); } } const tokenData = await tokenResponse.json(); const token = tokenData.token; if (!token) { throw new Error('No token received from NGINX Proxy Manager'); } // Now test the connection using the token const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); console.error('NGINX connection error:', errorText); try { const errorJson = JSON.parse(errorText); throw new Error(errorJson.message || 'Failed to connect to NGINX Proxy Manager'); } catch (e) { throw new Error(`Failed to connect to NGINX Proxy Manager: ${errorText}`); } } return { success: true }; } catch (error) { console.error('Error checking NGINX connection:', error); return { success: false, error: error.message || 'Error checking NGINX connection' }; } } async function checkPortainerConnection() { try { // Get Portainer settings from the template const portainerSettings = { url: window.portainerSettings?.url || '', api_key: window.portainerSettings?.api_key || '' }; // Debug log the settings (without API key) console.log('Portainer Settings:', { url: portainerSettings.url, hasApiKey: !!portainerSettings.api_key }); // Check if any required field is missing if (!portainerSettings.url || !portainerSettings.api_key) { console.error('Missing Portainer settings:', portainerSettings); return { success: false, message: 'Portainer settings are not configured. Please configure Portainer settings in the admin panel.' }; } const response = await fetch('/api/admin/test-portainer-connection', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ url: portainerSettings.url, api_key: portainerSettings.api_key }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to connect to Portainer'); } return { success: true, message: 'Successfully connected to Portainer' }; } catch (error) { console.error('Portainer connection error:', error); return { success: false, message: error.message || 'Failed to connect to Portainer' }; } } function updateStatus(step, message, type = 'info', details = '') { const statusElement = document.getElementById(`${step}Status`); const detailsElement = document.getElementById(`${step}Details`); if (statusElement) { // Remove any existing status classes statusElement.classList.remove('text-info', 'text-success', 'text-danger'); // Add appropriate class based on type switch (type) { case 'success': statusElement.classList.add('text-success'); break; case 'error': statusElement.classList.add('text-danger'); break; default: statusElement.classList.add('text-info'); } statusElement.textContent = message; } if (detailsElement) { detailsElement.innerHTML = details; } } async function createProxyHost(domains, port, sslCertificateId) { try { // Get NGINX settings from the template const nginxSettings = { url: window.nginxSettings?.url || '', username: window.nginxSettings?.username || '', password: window.nginxSettings?.password || '' }; console.log('NGINX Settings:', { ...nginxSettings, password: '***' }); // Update status to show we're getting the token updateStatus('proxy', 'Getting authentication token...', 'info'); // First, get the JWT token from NGINX const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: nginxSettings.username, secret: nginxSettings.password }) }); console.log('Token Response Status:', tokenResponse.status); console.log('Token Response Headers:', Object.fromEntries(tokenResponse.headers.entries())); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error('Token Error Response:', errorText); try { const errorJson = JSON.parse(errorText); throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`); } catch (e) { throw new Error(`Failed to authenticate with NGINX: ${errorText}`); } } const tokenData = await tokenResponse.json(); console.log('Token Data:', { ...tokenData, token: tokenData.token ? '***' : null }); const token = tokenData.token; if (!token) { throw new Error('No token received from NGINX Proxy Manager'); } // Store the token in sessionStorage for later use sessionStorage.setItem('nginxToken', token); // Check if a proxy host already exists with the same properties const proxyHostsResponse = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (!proxyHostsResponse.ok) { throw new Error('Failed to fetch existing proxy hosts'); } const proxyHosts = await proxyHostsResponse.json(); const existingProxy = proxyHosts.find(ph => { const sameDomains = Array.isArray(ph.domain_names) && ph.domain_names.length === domains.length && domains.every(d => ph.domain_names.includes(d)); return ( sameDomains && ph.forward_scheme === 'http' && ph.forward_host === '192.168.68.124' && parseInt(ph.forward_port) === parseInt(port) ); }); let result; if (existingProxy) { console.log('Found existing proxy host:', existingProxy); result = existingProxy; } else { // Update status to show we're creating the proxy host updateStatus('proxy', 'Creating proxy host configuration...', 'info'); const proxyHostData = { domain_names: domains, forward_scheme: 'http', forward_host: '192.168.68.124', forward_port: parseInt(port), ssl_forced: false, caching_enabled: true, block_exploits: true, allow_websocket_upgrade: true, http2_support: false, hsts_enabled: false, hsts_subdomains: false, certificate_id: sslCertificateId, meta: { letsencrypt_agree: true, dns_challenge: false } }; console.log('Creating proxy host with data:', proxyHostData); // Create the proxy host with NGINX const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(proxyHostData) }); console.log('Proxy Host Response Status:', response.status); console.log('Proxy Host Response Headers:', Object.fromEntries(response.headers.entries())); if (!response.ok) { const errorText = await response.text(); console.error('Proxy Host Error Response:', errorText); try { const errorJson = JSON.parse(errorText); const errorMessage = errorJson.error?.message || errorText; // Check if the error is about a domain already being in use if (errorMessage.includes('is already in use')) { const domain = errorMessage.split(' ')[0]; throw new Error(`Domain ${domain} is already configured in NGINX Proxy Manager. Please remove it from NGINX Proxy Manager and try again.`); } throw new Error(`Failed to create proxy host: ${errorMessage}`); } catch (e) { if (e.message.includes('is already configured in NGINX Proxy Manager')) { throw e; // Re-throw the domain in use error } throw new Error(`Failed to create proxy host: ${errorText}`); } } result = await response.json(); console.log('Proxy Host Creation Result:', result); } // Create a detailed success message with NGINX Proxy results const successDetails = `
NGINX Proxy Results
Property Value
Proxy Host ID ${result.id || 'N/A'}
Domains ${domains.join(', ')}
Forward Scheme http
Forward Host 192.168.68.124
Forward Port ${parseInt(port)}
SSL Status Forced
SSL Certificate Using Certificate ID: ${sslCertificateId}
Security Features Block Exploits HSTS HTTP/2
Performance Caching WebSocket
`; // Update the proxy step to show success and add the results const proxyStep = document.querySelectorAll('.step-item')[3]; proxyStep.classList.remove('active'); proxyStep.classList.add('completed'); const statusText = proxyStep.querySelector('.step-status'); statusText.textContent = existingProxy ? 'Using existing proxy host' : 'Successfully created proxy host'; statusText.after(document.createRange().createContextualFragment(successDetails)); return { success: true, data: result }; } catch (error) { console.error('Error creating proxy host:', error); // Update status with error message updateStatus('proxy', `Failed: ${error.message}`, 'error'); return { success: false, error: error.message }; } } async function generateSSLCertificate(domains) { try { // Get NGINX settings from the template const nginxSettings = { url: window.nginxSettings?.url || '', username: window.nginxSettings?.username || '', password: window.nginxSettings?.password || '', email: window.nginxSettings?.email || '' }; // Get a fresh token from NGINX const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: nginxSettings.username, secret: nginxSettings.password }) }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error('Token Error Response:', errorText); throw new Error(`Failed to authenticate with NGINX: ${errorText}`); } const tokenData = await tokenResponse.json(); const token = tokenData.token; if (!token) { throw new Error('No token received from NGINX Proxy Manager'); } // First, check if a certificate already exists for these domains const checkResponse = await fetch(`${nginxSettings.url}/api/nginx/certificates`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); if (!checkResponse.ok) { throw new Error('Failed to check existing certificates'); } const existingCertificates = await checkResponse.json(); const existingCertificate = existingCertificates.find(cert => { const certDomains = cert.domain_names || []; return domains.every(domain => certDomains.includes(domain)) && certDomains.length === domains.length; }); let result; let usedExisting = false; if (existingCertificate) { console.log('Found existing certificate:', existingCertificate); result = existingCertificate; usedExisting = true; } else { // Create the SSL certificate directly with NGINX const requestBody = { domain_names: domains, meta: { letsencrypt_email: nginxSettings.email, letsencrypt_agree: true, dns_challenge: false }, provider: 'letsencrypt' }; console.log('Request Body:', requestBody); const response = await fetch(`${nginxSettings.url}/api/nginx/certificates`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(requestBody) }); console.log('Response Status:', response.status); console.log('Response Headers:', Object.fromEntries(response.headers.entries())); if (!response.ok) { const errorText = await response.text(); console.error('Certificate creation error:', errorText); throw new Error(`Failed to generate SSL certificate: ${errorText}`); } result = await response.json(); console.log('Certificate creation result:', result); } // Update the SSL step to show success const sslStep = document.querySelectorAll('.step-item')[2]; sslStep.classList.remove('active'); sslStep.classList.add('completed'); const sslStatusText = sslStep.querySelector('.step-status'); sslStatusText.textContent = usedExisting ? 'Using existing SSL certificate' : 'SSL certificate generated successfully'; // Always add SSL certificate details const sslDetails = document.createElement('div'); sslDetails.className = 'mt-3'; sslDetails.innerHTML = `
SSL Certificate Details
Property Value
Certificate ID ${result.id || 'N/A'}
Domains ${(result.domain_names || domains).join(', ')}
Provider ${result.provider || 'Let\'s Encrypt'}
`; sslStatusText.after(sslDetails); return { success: true, data: { certificate: result } }; } catch (error) { console.error('Error generating SSL certificate:', error); return { success: false, error: error.message }; } } function scrollToStep(stepElement) { const container = document.querySelector('.launch-steps-container'); const containerRect = container.getBoundingClientRect(); const elementRect = stepElement.getBoundingClientRect(); // Calculate the scroll position to center the element in the container const scrollTop = elementRect.top - containerRect.top - (containerRect.height / 2) + (elementRect.height / 2); // Smooth scroll to the element container.scrollTo({ top: container.scrollTop + scrollTop, behavior: 'smooth' }); } function updateStep(stepNumber, title, description) { return new Promise((resolve) => { // Update the current step in the header document.getElementById('currentStep').textContent = title; document.getElementById('stepDescription').textContent = description; // Calculate progress based on total number of steps (9 steps total) const totalSteps = 9; const progress = ((stepNumber - 1) / (totalSteps - 1)) * 100; const progressBar = document.getElementById('launchProgress'); progressBar.style.width = `${progress}%`; progressBar.textContent = `${Math.round(progress)}%`; // Update step items const steps = document.querySelectorAll('.step-item'); steps.forEach((item, index) => { const step = index + 1; item.classList.remove('active', 'completed', 'failed'); if (step < stepNumber) { item.classList.add('completed'); item.querySelector('.step-status').textContent = 'Completed'; } else if (step === stepNumber) { item.classList.add('active'); item.querySelector('.step-status').textContent = description; // Scroll to the active step scrollToStep(item); } }); // Simulate some work being done setTimeout(resolve, 1000); }); } function showError(message) { const errorContainer = document.getElementById('errorContainer'); const errorMessage = document.getElementById('errorMessage'); errorMessage.textContent = message; errorContainer.style.display = 'block'; // Update the current step to show error const currentStep = document.querySelector('.step-item.active'); if (currentStep) { currentStep.classList.add('failed'); currentStep.querySelector('.step-status').textContent = 'Failed: ' + message; } } function retryLaunch() { // Reload the page to start over window.location.reload(); } // Add new function to download docker-compose.yml async function downloadDockerCompose(repo, branch) { try { const response = await fetch('/api/admin/download-docker-compose', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ repository: repo, branch: branch }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to download docker-compose.yml'); } const result = await response.json(); return { success: true, content: result.content }; } catch (error) { console.error('Error downloading docker-compose.yml:', error); return { success: false, error: error.message }; } } // Add new function to deploy stack async function deployStack(dockerComposeContent, stackName, port) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10 minutes timeout const response = await fetch('/api/admin/deploy-stack', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ name: `docupulse_${port}`, StackFileContent: dockerComposeContent, Env: [ { name: 'PORT', value: port.toString() }, { name: 'ISMASTER', value: 'false' } ] }), signal: controller.signal }); clearTimeout(timeoutId); // Clear the timeout if the request completes if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to deploy stack'); } const result = await response.json(); return { success: true, data: result }; } catch (error) { console.error('Error deploying stack:', error); return { success: false, error: error.message }; } } // Add new function to save instance data async function saveInstanceData(instanceData) { try { console.log('Saving instance data:', instanceData); // First check if instance already exists const checkResponse = await fetch('/instances'); const text = await checkResponse.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // Look for an existing instance with the same name const existingInstance = Array.from(doc.querySelectorAll('table tbody tr')).find(row => { const nameCell = row.querySelector('td:first-child'); return nameCell && nameCell.textContent.trim() === instanceData.port; }); if (existingInstance) { console.log('Instance already exists:', instanceData.port); return { success: true, data: { name: instanceData.port, company: 'loading...', rooms_count: 0, conversations_count: 0, data_size: 0.0, payment_plan: 'Basic', main_url: `https://${instanceData.domains[0]}`, status: 'inactive', port: instanceData.port, stack_id: instanceData.stack_id, stack_name: instanceData.stack_name, repository: instanceData.repository, branch: instanceData.branch } }; } // If instance doesn't exist, create it const response = await fetch('/instances/add', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ name: instanceData.port, company: 'loading...', rooms_count: 0, conversations_count: 0, data_size: 0.0, payment_plan: 'Basic', main_url: `https://${instanceData.domains[0]}`, status: 'inactive', port: instanceData.port, stack_id: instanceData.stack_id, stack_name: instanceData.stack_name, repository: instanceData.repository, branch: instanceData.branch }) }); if (!response.ok) { const errorText = await response.text(); console.error('Error response:', errorText); throw new Error(`Failed to save instance data: ${response.status} ${response.statusText}`); } const result = await response.json(); console.log('Instance data saved:', result); return result; } catch (error) { console.error('Error saving instance data:', error); throw error; } } async function checkInstanceHealth(instanceUrl) { const maxRetries = 5; let currentAttempt = 1; while (currentAttempt <= maxRetries) { try { // First get the instance ID from the database const response = await fetch('/instances'); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // Find the instance row that matches our 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 instanceId = statusBadge.dataset.instanceId; // Now use the instance ID to check status const statusResponse = await fetch(`/instances/${instanceId}/status`); if (!statusResponse.ok) { throw new Error(`Health check failed with status ${statusResponse.status}`); } const data = await statusResponse.json(); // Update the health check step const healthStep = document.querySelectorAll('.step-item')[8]; // Adjust index based on your steps healthStep.classList.remove('active'); healthStep.classList.add('completed'); const statusText = healthStep.querySelector('.step-status'); if (data.status === 'active') { statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries})`; return { success: true, data: data }; } else { throw new Error('Instance is not healthy'); } } catch (error) { console.error(`Health check attempt ${currentAttempt} failed:`, error); // Update status to show current attempt const healthStep = document.querySelectorAll('.step-item')[8]; const statusText = healthStep.querySelector('.step-status'); statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}): ${error.message}`; if (currentAttempt === maxRetries) { return { success: false, error: `Health check failed after ${maxRetries} attempts: ${error.message}` }; } // Wait 5 seconds before next attempt await new Promise(resolve => setTimeout(resolve, 5000)); currentAttempt++; } } }