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 Cloudflare connection check step const cloudflareStep = document.createElement('div'); cloudflareStep.className = 'step-item'; cloudflareStep.innerHTML = `
Checking Cloudflare Connection

Verifying Cloudflare API connection...

`; stepsContainer.appendChild(cloudflareStep); // Add DNS record creation step const dnsCreateStep = document.createElement('div'); dnsCreateStep.className = 'step-item'; dnsCreateStep.innerHTML = `
Creating DNS Records

Setting up domain DNS records in Cloudflare...

`; stepsContainer.appendChild(dnsCreateStep); // 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); // Add Authentication step const authStep = document.createElement('div'); authStep.className = 'step-item'; authStep.innerHTML = `
Instance Authentication

Setting up instance authentication...

`; stepsContainer.appendChild(authStep); // Add Apply Company Information step const companyStep = document.createElement('div'); companyStep.className = 'step-item'; companyStep.innerHTML = `
Apply Company Information

Configuring company details...

`; stepsContainer.appendChild(companyStep); // Add Apply Colors step const colorsStep = document.createElement('div'); colorsStep.className = 'step-item'; colorsStep.innerHTML = `
Apply Colors

Configuring color scheme...

`; stepsContainer.appendChild(colorsStep); // Add Update Admin Credentials step const credentialsStep = document.createElement('div'); credentialsStep.className = 'step-item'; credentialsStep.innerHTML = `
Update Admin Credentials

Setting up admin account...

`; stepsContainer.appendChild(credentialsStep); // Add Copy SMTP Settings step const smtpStep = document.createElement('div'); smtpStep.className = 'step-item'; smtpStep.innerHTML = `
Copy SMTP Settings

Configuring email settings...

`; stepsContainer.appendChild(smtpStep); // Add Send Completion Email step const emailStep = document.createElement('div'); emailStep.className = 'step-item'; emailStep.innerHTML = `
Send Completion Email

Sending notification to client...

`; stepsContainer.appendChild(emailStep); // Add Download Launch Report step const reportStep = document.createElement('div'); reportStep.className = 'step-item'; reportStep.innerHTML = `
Download Launch Report

Preparing launch report...

`; stepsContainer.appendChild(reportStep); } async function startLaunch(data) { const launchReport = { startedAt: new Date().toISOString(), steps: [] }; try { // Step 1: Check Cloudflare connection await updateStep(1, 'Checking Cloudflare Connection', 'Verifying Cloudflare API connection...'); const cloudflareResult = await checkCloudflareConnection(); launchReport.steps.push({ step: 'Cloudflare Connection', status: cloudflareResult.success ? 'success' : 'error', details: cloudflareResult }); 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); launchReport.steps.push({ step: 'DNS Records', status: dnsCreateResult.success ? 'success' : 'error', details: dnsCreateResult }); 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 = `
DNS Record Creation Results
${Object.entries(dnsCreateResult.results).map(([domain, result]) => ` `).join('')}
Domain Status Message
${domain} ${result.status} ${result.message}
`; 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); launchReport.steps.push({ step: 'DNS Check', status: dnsResult.success ? 'success' : 'error', details: dnsResult }); if (!dnsResult.success) { throw new Error(dnsResult.error || 'Failed to check DNS records'); } // 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')[2]; 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 4: Check NGINX connection await updateStep(4, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...'); const nginxResult = await checkNginxConnection(); launchReport.steps.push({ step: 'NGINX Connection', status: nginxResult.success ? 'success' : 'error', details: nginxResult }); 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')[3]; nginxStep.classList.remove('active'); nginxStep.classList.add('completed'); nginxStep.querySelector('.step-status').textContent = 'Successfully connected to NGINX Proxy Manager'; // Step 5: Generate SSL Certificate await updateStep(5, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...'); const sslResult = await generateSSLCertificate(data.webAddresses); launchReport.steps.push({ step: 'SSL Certificate', status: sslResult.success ? 'success' : 'error', details: sslResult }); if (!sslResult.success) { throw new Error(sslResult.error || 'Failed to generate SSL certificate'); } // 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); launchReport.steps.push({ step: 'Proxy Host', status: proxyResult.success ? 'success' : 'error', details: proxyResult }); if (!proxyResult.success) { throw new Error(proxyResult.error || 'Failed to create proxy host'); } // Step 7: Check Portainer connection await updateStep(7, 'Checking Portainer Connection', 'Verifying connection to Portainer...'); const portainerResult = await checkPortainerConnection(); launchReport.steps.push({ step: 'Portainer Connection', status: portainerResult.success ? 'success' : 'error', details: portainerResult }); 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')[6]; portainerStep.classList.remove('active'); portainerStep.classList.add('completed'); portainerStep.querySelector('.step-status').textContent = portainerResult.message; // 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); launchReport.steps.push({ step: 'Docker Compose', status: dockerComposeResult.success ? 'success' : 'error', details: dockerComposeResult }); if (!dockerComposeResult.success) { throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml'); } // Set global version variables for deployment window.currentDeploymentVersion = dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'; window.currentDeploymentCommit = dockerComposeResult.commit_hash || 'unknown'; window.currentDeploymentBranch = data.branch; // Update the step to show success 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'; // 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 9: Deploy Stack await updateStep(9, 'Deploying Stack', 'Launching your application stack...'); // Add progress indicator for stack deployment const stackDeployStepElement = document.querySelectorAll('.step-item')[8]; const stackProgressDiv = document.createElement('div'); stackProgressDiv.className = 'mt-2'; stackProgressDiv.innerHTML = `
0%
Initiating stack deployment... `; stackDeployStepElement.querySelector('.step-content').appendChild(stackProgressDiv); const stackName = generateStackName(data.port); const stackResult = await deployStack(dockerComposeResult.content, stackName, data.port); launchReport.steps.push({ step: 'Stack Deployment', status: stackResult.success ? 'success' : 'error', details: stackResult }); // Handle different stack deployment scenarios if (!stackResult.success) { // Check if this is a timeout but the stack might still be deploying if (stackResult.error && stackResult.error.includes('timed out')) { console.log('Stack deployment timed out, but may still be in progress'); // Update the step to show warning instead of error const stackDeployStep = document.querySelectorAll('.step-item')[8]; stackDeployStep.classList.remove('active'); stackDeployStep.classList.add('warning'); stackDeployStep.querySelector('.step-status').textContent = 'Stack deployment timed out but may still be in progress'; // Add a note about the timeout const timeoutNote = document.createElement('div'); timeoutNote.className = 'alert alert-warning mt-2'; timeoutNote.innerHTML = ` Note: The stack deployment request timed out, but the deployment may still be in progress. You can check the status in your Portainer dashboard or wait a few minutes and refresh this page. `; stackDeployStep.querySelector('.step-content').appendChild(timeoutNote); // Continue with the process using the available data stackResult.data = stackResult.data || { name: stackName, status: 'creating', id: null }; } else { throw new Error(stackResult.error || 'Failed to deploy stack'); } } // Update the step to show success const stackDeployStep = document.querySelectorAll('.step-item')[8]; stackDeployStep.classList.remove('active'); stackDeployStep.classList.add('completed'); stackDeployStep.querySelector('.step-status').textContent = stackResult.data.status === 'existing' ? 'Using existing stack' : stackResult.data.status === 'active' ? 'Successfully deployed and activated 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 || 'Will be determined during deployment'}
Status ${stackResult.data.status === 'existing' ? 'Existing' : stackResult.data.status === 'active' ? 'Active' : 'Deployed'}
`; stackDeployStep.querySelector('.step-content').appendChild(stackDetails); // Save instance data await updateStep(10, 'Saving Instance Data', 'Storing instance information...'); try { // Get the launch data from sessionStorage to access pricing tier info const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData') || '{}'); const instanceData = { name: data.instanceName, port: data.port, domains: data.webAddresses, stack_id: stackResult.data.id || null, // May be null if we got a 504 stack_name: stackResult.data.name, status: stackResult.data.status, repository: data.repository, branch: data.branch, deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown', deployed_branch: data.branch, payment_plan: launchData.pricingTier?.name || 'Basic' // Use the selected pricing tier name }; console.log('Saving instance data:', instanceData); const saveResult = await saveInstanceData(instanceData); console.log('Save result:', saveResult); // Update step with version information const versionInfo = dockerComposeResult.commit_hash ? `Instance data saved successfully. Version: ${dockerComposeResult.commit_hash.substring(0, 8)}` : 'Instance data saved successfully'; await updateStep(10, 'Saving Instance Data', versionInfo); } catch (error) { console.error('Error saving instance data:', error); await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`); throw error; } // Update the step to show success const saveDataStep = document.querySelectorAll('.step-item')[9]; saveDataStep.classList.remove('active'); saveDataStep.classList.add('completed'); saveDataStep.querySelector('.step-status').textContent = 'Successfully saved instance data'; // After saving instance data, add the health check step await updateStep(11, 'Health Check', 'Verifying instance health...'); // Add a progress indicator const healthStepElement = document.querySelectorAll('.step-item')[10]; const progressDiv = document.createElement('div'); progressDiv.className = 'mt-2'; progressDiv.innerHTML = `
0%
Starting health check... `; healthStepElement.querySelector('.step-content').appendChild(progressDiv); 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 healthStepElement2 = document.querySelectorAll('.step-item')[10]; 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) { healthStepElement2.classList.remove('failed'); healthStepElement2.classList.add('completed'); retryButton.remove(); } else { retryButton.disabled = false; retryButton.innerHTML = ' Retry Health Check'; } }; healthStepElement2.querySelector('.step-content').appendChild(retryButton); } // After health check, add authentication step await updateStep(12, 'Instance Authentication', 'Setting up instance authentication...'); const authResult = await authenticateInstance(`https://${data.webAddresses[0]}`, data.instanceId); if (!authResult.success) { throw new Error(`Authentication failed: ${authResult.error}`); } // Update the auth step to show success const authStep = document.querySelectorAll('.step-item')[11]; authStep.classList.remove('active'); authStep.classList.add('completed'); authStep.querySelector('.step-status').textContent = authResult.alreadyAuthenticated ? 'Instance is already authenticated' : 'Successfully authenticated instance'; // Add authentication details const authDetails = document.createElement('div'); authDetails.className = 'mt-3'; authDetails.innerHTML = `
Authentication Details
Property Value
Status Authenticated
Default Admin administrator@docupulse.com
Connection Type Management API Key
Authentication Type ${authResult.alreadyAuthenticated ? 'Existing' : 'New'}
`; authStep.querySelector('.step-content').appendChild(authDetails); // Step 13: Apply Company Information await updateStep(13, 'Apply Company Information', 'Configuring company details...'); const companyResult = await applyCompanyInformation(`https://${data.webAddresses[0]}`, data.company); launchReport.steps.push({ step: 'Company Information', status: companyResult.success ? 'success' : 'error', details: companyResult }); if (!companyResult.success) { throw new Error(`Company information application failed: ${companyResult.error}`); } // Update the company step to show success const companyStep = document.querySelectorAll('.step-item')[12]; companyStep.classList.remove('active'); companyStep.classList.add('completed'); companyStep.querySelector('.step-status').textContent = 'Successfully applied company information'; // Add company details const companyDetails = document.createElement('div'); companyDetails.className = 'mt-3'; companyDetails.innerHTML = `
Company Information Applied
Property Value
Company Name ${data.company.name || 'Not set'}
Industry ${data.company.industry || 'Not set'}
Email ${data.company.email || 'Not set'}
Website ${data.company.website || 'Not set'}
Address ${data.company.streetAddress || 'Not set'}, ${data.company.city || ''}, ${data.company.state || ''} ${data.company.zipCode || ''}
`; companyStep.querySelector('.step-content').appendChild(companyDetails); // Step 14: Apply Colors await updateStep(14, 'Apply Colors', 'Configuring color scheme...'); const colorsResult = await applyColors(`https://${data.webAddresses[0]}`, data.colors); launchReport.steps.push({ step: 'Colors', status: colorsResult.success ? 'success' : 'error', details: colorsResult }); if (!colorsResult.success) { throw new Error(`Colors application failed: ${colorsResult.error}`); } // Update the colors step to show success const colorsStep = document.querySelectorAll('.step-item')[13]; colorsStep.classList.remove('active'); colorsStep.classList.add('completed'); colorsStep.querySelector('.step-status').textContent = 'Successfully applied color scheme'; // Add colors details const colorsDetails = document.createElement('div'); colorsDetails.className = 'mt-3'; colorsDetails.innerHTML = `
Color Scheme Applied
Property Value Preview
Primary Color ${data.colors.primary || 'Not set'}
Secondary Color ${data.colors.secondary || 'Not set'}
`; colorsStep.querySelector('.step-content').appendChild(colorsDetails); // 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); launchReport.steps.push({ step: 'Admin Credentials', status: credentialsResult.success ? 'success' : 'error', details: credentialsResult }); if (!credentialsResult.success) { throw new Error(`Admin credentials update failed: ${credentialsResult.error}`); } // Update the credentials step to show success const credentialsStep = document.querySelectorAll('.step-item')[14]; credentialsStep.classList.remove('active'); credentialsStep.classList.add('completed'); if (credentialsResult.data.already_updated) { credentialsStep.querySelector('.step-status').textContent = 'Admin credentials already updated'; } else { credentialsStep.querySelector('.step-status').textContent = 'Successfully updated admin credentials'; } // Add credentials details const credentialsDetails = document.createElement('div'); credentialsDetails.className = 'mt-3'; credentialsDetails.innerHTML = `
${credentialsResult.data.already_updated ? 'Admin Credentials Status' : 'Admin Credentials Updated'}
Property Value
Email Address ${credentialsResult.data.email || 'Not set'}
Username ${credentialsResult.data.username || 'administrator'}
Role Administrator
Password Setup Reset Link Pending
Status ${credentialsResult.data.already_updated ? 'Already Updated' : 'Updated'}
${credentialsResult.data.already_updated ? 'Note:' : 'Security Setup Required:'} ${credentialsResult.data.already_updated ? 'Admin credentials were already updated from the default settings.' : 'A secure password reset link will be sent to the admin email address in the completion email.'}
`; credentialsStep.querySelector('.step-content').appendChild(credentialsDetails); // Step 16: Copy SMTP Settings await updateStep(16, 'Copy SMTP Settings', 'Configuring email settings...'); const smtpResult = await copySmtpSettings(`https://${data.webAddresses[0]}`); launchReport.steps.push({ step: 'SMTP Settings', status: smtpResult.success ? 'success' : 'error', details: smtpResult }); if (!smtpResult.success) { throw new Error(`SMTP settings copy failed: ${smtpResult.error}`); } // Update the SMTP step to show success const smtpStep = document.querySelectorAll('.step-item')[15]; smtpStep.classList.remove('active'); smtpStep.classList.add('completed'); smtpStep.querySelector('.step-status').textContent = 'Successfully copied SMTP settings'; // Add SMTP details const smtpDetails = document.createElement('div'); smtpDetails.className = 'mt-3'; smtpDetails.innerHTML = `
SMTP Settings Copied
Property Value
SMTP Host ${smtpResult.data.smtp_host || 'Not set'}
SMTP Port ${smtpResult.data.smtp_port || 'Not set'}
Security ${smtpResult.data.smtp_security || 'None'}
Username ${smtpResult.data.smtp_username || 'Not set'}
From Email ${smtpResult.data.smtp_from_email || 'Not set'}
From Name ${smtpResult.data.smtp_from_name || 'Not set'}
Status Copied Successfully
Email Configuration Complete! The launched instance now has the same SMTP settings as the master instance and can send emails independently.
`; smtpStep.querySelector('.step-content').appendChild(smtpDetails); // 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); launchReport.steps.push({ step: 'Completion Email', status: emailResult.success ? 'success' : 'error', details: emailResult }); if (!emailResult.success) { throw new Error(`Email sending failed: ${emailResult.error}`); } // Update the email step to show success const emailStep = document.querySelectorAll('.step-item')[16]; emailStep.classList.remove('active'); emailStep.classList.add('completed'); emailStep.querySelector('.step-status').textContent = 'Successfully sent completion email'; // Add email details const emailDetails = document.createElement('div'); emailDetails.className = 'mt-3'; emailDetails.innerHTML = `
Completion Email Sent
Property Value
Recipient ${data.company.email || 'Not set'}
Subject Your DocuPulse Instance is Ready!
Status Sent Successfully
Instance URL https://${data.webAddresses[0]}
Password Reset Link Included

HTML Version
Plain Text Version
Your DocuPulse Instance is Ready! Dear ${data.company.name || 'Valued Customer'}, Great news! Your DocuPulse instance has been successfully deployed and configured. INSTANCE DETAILS: - Instance URL: https://${data.webAddresses[0]} - Company Name: ${data.company.name || 'Not set'} - Industry: ${data.company.industry || 'Not set'} - Deployment Date: ${new Date().toLocaleString()} ACCOUNT ACCESS: - Email Address: ${data.company.email || 'Not set'} - Username: administrator SECURITY SETUP REQUIRED: For your security, you need to set up your password. Password Reset Link: [Secure reset link included] Password Reset Link Expires: 24 hours WHAT'S BEEN CONFIGURED: ✓ Secure SSL certificate for HTTPS access ✓ Company information and branding ✓ Custom color scheme ✓ Admin account created ✓ Document management system ready NEXT STEPS: 1. Click the password reset link above 2. Create your secure password 3. Return to your instance and log in 4. Explore your new DocuPulse platform 5. Start uploading and organizing your documents 6. Invite team members to collaborate If you have any questions or need assistance, please don't hesitate to contact our support team. Thank you for choosing DocuPulse!
Launch Complete! Your DocuPulse instance has been successfully deployed and configured. The client has been notified via email with all necessary login information and a secure password reset link.
`; emailStep.querySelector('.step-content').appendChild(emailDetails); // Update the top section to show completion document.getElementById('currentStep').textContent = 'Launch Complete!'; document.getElementById('stepDescription').textContent = 'Your DocuPulse instance has been successfully deployed and configured.'; const progressBar = document.getElementById('launchProgress'); progressBar.style.width = '100%'; progressBar.textContent = '100%'; // Scroll to the bottom of the steps container const stepsContainer = document.getElementById('stepsContainer'); stepsContainer.scrollTo({ top: stepsContainer.scrollHeight, behavior: 'smooth' }); // Step 18: Download Launch Report await updateStep(18, 'Download Launch Report', 'Download a full report of the launch process.'); const reportStep = document.querySelectorAll('.step-item')[17]; reportStep.classList.remove('active'); reportStep.classList.add('completed'); reportStep.querySelector('.step-status').textContent = 'Download your launch report below.'; // Prepare the report data launchReport.completedAt = new Date().toISOString(); launchReport.instance = { domains: data.webAddresses, company: data.company, port: data.port }; // Create the download button const downloadReportBtn = document.createElement('button'); downloadReportBtn.className = 'btn btn-success mt-3'; downloadReportBtn.innerHTML = ' Download Launch Report'; downloadReportBtn.onclick = () => { const blob = new Blob([JSON.stringify(launchReport, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `docupulse_launch_report_${data.instanceName || 'instance'}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }; reportStep.querySelector('.step-content').appendChild(downloadReportBtn); } catch (error) { console.error('Launch failed:', error); 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-cloudflare-connection', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.csrfToken } }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to check Cloudflare connection'); } return await response.json(); } catch (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; } } 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')[5]; 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')[4]; 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 (18 steps total) const totalSteps = 18; 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, commit_hash: result.commit_hash, latest_tag: result.latest_tag }; } catch (error) { console.error('Error downloading docker-compose.yml:', 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); // Update existing instance with new data const updateResponse = await fetch('/api/admin/save-instance', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ name: instanceData.port, port: instanceData.port, domains: instanceData.domains, stack_id: instanceData.stack_id || '', stack_name: instanceData.stack_name, status: instanceData.status, repository: instanceData.repository, branch: instanceData.branch, deployed_version: instanceData.deployed_version, deployed_branch: instanceData.deployed_branch, payment_plan: instanceData.payment_plan || 'Basic' }) }); if (!updateResponse.ok) { const errorText = await updateResponse.text(); console.error('Error updating instance:', errorText); throw new Error(`Failed to update instance data: ${updateResponse.status} ${updateResponse.statusText}`); } const updateResult = await updateResponse.json(); console.log('Instance updated:', updateResult); return { success: true, data: updateResult.data }; } // If instance doesn't exist, create it const response = await fetch('/api/admin/save-instance', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ name: instanceData.port, port: instanceData.port, domains: instanceData.domains, stack_id: instanceData.stack_id || '', stack_name: instanceData.stack_name, status: instanceData.status, repository: instanceData.repository, branch: instanceData.branch, deployed_version: instanceData.deployed_version, deployed_branch: instanceData.deployed_branch, payment_plan: instanceData.payment_plan || 'Basic' }) }); 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 = 120; // 120 retries * 5 seconds = 10 minutes total const baseDelay = 5000; // 5 seconds base delay let currentAttempt = 1; const startTime = Date.now(); const maxTotalTime = 10 * 60 * 1000; // 10 minutes in milliseconds while (currentAttempt <= maxRetries) { try { // Check if we've exceeded the total timeout if (Date.now() - startTime > maxTotalTime) { throw new Error('Health check timeout: 10 minutes exceeded'); } // 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 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'); 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% 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'); } } 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 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 if (progressBar && progressText) { progressBar.classList.remove('progress-bar-animated'); progressBar.classList.add('bg-danger'); progressText.textContent = `Health check failed after ${currentAttempt} attempts (${elapsedTime}s)`; } return { success: false, error: `Health check failed after ${currentAttempt} attempts (${elapsedTime}s): ${error.message}` }; } // Wait before next attempt (5 seconds base delay) await new Promise(resolve => setTimeout(resolve, baseDelay)); currentAttempt++; // Update progress bar in real-time updateHealthProgress(currentAttempt, maxRetries, elapsedTime); } } } function updateHealthProgress(currentAttempt, maxRetries, elapsedTime) { 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)`; } } 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); const response = await fetch('/api/admin/apply-company-information', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ instance_url: instanceUrl, company_data: company }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to apply company information: ${errorText}`); } const result = await response.json(); console.log('Company information applied successfully:', result); return { success: true, message: result.message, data: result.data }; } catch (error) { console.error('Error applying company information:', error); return { success: false, error: error.message }; } } async function applyColors(instanceUrl, colors) { try { console.log('Applying colors to:', instanceUrl); const response = await fetch('/api/admin/apply-colors', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ instance_url: instanceUrl, colors_data: colors }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to apply colors: ${errorText}`); } const result = await response.json(); console.log('Colors applied successfully:', result); return { success: true, message: result.message, data: result.data }; } catch (error) { console.error('Error applying colors:', error); return { success: false, error: error.message }; } } async function updateAdminCredentials(instanceUrl, email) { try { console.log('Updating admin credentials for:', instanceUrl); const response = await fetch('/api/admin/update-admin-credentials', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ instance_url: instanceUrl, email: email }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to update admin credentials: ${errorText}`); } const result = await response.json(); console.log('Admin credentials updated successfully:', result); return { success: true, message: result.message, data: result.data }; } catch (error) { console.error('Error updating admin credentials:', error); return { success: false, error: error.message }; } } async function copySmtpSettings(instanceUrl) { try { console.log('Copying SMTP settings to:', instanceUrl); const response = await fetch('/api/admin/copy-smtp-settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ instance_url: instanceUrl }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to copy SMTP settings: ${errorText}`); } const result = await response.json(); console.log('SMTP settings copied successfully:', result); return { success: true, message: result.message, data: result.data }; } catch (error) { console.error('Error copying SMTP settings:', error); return { success: false, error: error.message }; } } async function sendCompletionEmail(instanceUrl, company, credentials) { try { console.log('Sending completion email to:', company.email); const response = await fetch('/api/admin/send-completion-email', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ instance_url: instanceUrl, company_data: company, credentials_data: credentials }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to send completion email: ${errorText}`); } const result = await response.json(); console.log('Email sent successfully:', result); return { success: true, message: result.message, data: result.data }; } catch (error) { console.error('Error sending completion email:', error); return { success: false, error: error.message }; } } // Add new function to check if stack exists async function checkStackExists(stackName) { try { const response = 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: stackName }) }); if (response.ok) { const result = await response.json(); return { exists: true, status: result.data?.status || 'unknown', data: result.data }; } else { return { exists: false, status: 'not_found' }; } } catch (error) { console.error('Error checking stack existence:', error); return { exists: false, status: 'error', error: error.message }; } } // Add new function to deploy stack async function deployStack(dockerComposeContent, stackName, port) { try { // Get the launch data from sessionStorage to access pricing tier info const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); // Fetch the pricing tier details to get the actual quota values let pricingTierDetails = null; if (launchData?.pricingTier?.id) { try { const pricingResponse = await fetch(`/api/admin/pricing-plans/${launchData.pricingTier.id}`, { method: 'GET', headers: { 'Accept': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }); if (pricingResponse.ok) { const pricingData = await pricingResponse.json(); if (pricingData.success) { pricingTierDetails = pricingData.plan; } } } catch (error) { console.warn('Failed to fetch pricing tier details:', error); } } // First, attempt to deploy the stack 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: stackName, StackFileContent: dockerComposeContent, Env: [ { name: 'PORT', value: port.toString() }, { name: 'ISMASTER', value: 'false' }, { 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() }, // Pricing tier environment variables with actual quota values { name: 'PRICING_TIER_ID', value: launchData?.pricingTier?.id?.toString() || '0' }, { name: 'PRICING_TIER_NAME', value: launchData?.pricingTier?.name || 'Unknown' }, { name: 'ROOM_QUOTA', value: pricingTierDetails?.room_quota?.toString() || '0' }, { name: 'CONVERSATION_QUOTA', value: pricingTierDetails?.conversation_quota?.toString() || '0' }, { name: 'STORAGE_QUOTA_GB', value: pricingTierDetails?.storage_quota_gb?.toString() || '0' }, { name: 'MANAGER_QUOTA', value: pricingTierDetails?.manager_quota?.toString() || '0' }, { name: 'ADMIN_QUOTA', value: pricingTierDetails?.admin_quota?.toString() || '0' } ] }) }); // Handle 504 Gateway Timeout as successful initiation if (response.status === 504) { console.log('Received 504 Gateway Timeout - stack creation may still be in progress'); // Update progress to show that we're now polling const progressBar = document.getElementById('stackProgress'); const progressText = document.getElementById('stackProgressText'); if (progressBar && progressText) { progressBar.style.width = '25%'; progressBar.textContent = '25%'; progressText.textContent = 'Stack creation initiated (timed out, but continuing to monitor)...'; } // Start polling immediately since the stack creation was initiated console.log('Starting to poll for stack status after 504 timeout...'); const pollResult = await pollStackStatus(stackName, 15 * 60 * 1000); // 15 minutes max return pollResult; } if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to deploy stack'); } const result = await response.json(); console.log('Stack deployment initiated:', result); // If stack is being created, poll for status if (result.data.status === 'creating') { console.log('Stack is being created, polling for status...'); const pollResult = await pollStackStatus(stackName, 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 deploying stack:', error); return { success: false, error: error.message }; } } // Function to poll stack status async function pollStackStatus(stackName, 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); return { success: false, error: `Invalid stack name: ${stackName}`, data: { name: stackName, status: 'error' } }; } console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`); // Update progress indicator const progressBar = document.getElementById('stackProgress'); const progressText = document.getElementById('stackProgressText'); while (Date.now() - startTime < maxWaitTime) { attempts++; console.log(`Polling attempt ${attempts} for stack: ${stackName}`); // Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0% const elapsed = Date.now() - startTime; const baseProgress = progressBar && progressBar.style.width === '25%' ? 25 : 0; const progress = Math.min(baseProgress + (elapsed / maxWaitTime) * 70, 95); // Cap at 95% until complete if (progressBar && progressText) { progressBar.style.width = `${progress}%`; progressBar.textContent = `${Math.round(progress)}%`; } try { const requestBody = { stack_name: stackName }; console.log(`Sending stack status check request:`, requestBody); const response = 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(requestBody), timeout: 30000 // 30 second timeout for status checks }); if (response.ok) { const result = await response.json(); console.log(`Stack status check result:`, result); if (result.data && result.data.status === 'active') { console.log(`Stack ${stackName} is now active!`); // Update progress to 100% if (progressBar && progressText) { progressBar.style.width = '100%'; progressBar.textContent = '100%'; progressBar.classList.remove('progress-bar-animated'); progressBar.classList.add('bg-success'); progressText.textContent = 'Stack is now active and running!'; } return { success: true, data: { name: stackName, id: result.data.stack_id, status: 'active' } }; } else if (result.data && result.data.status === 'partial') { console.log(`Stack ${stackName} 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...`); 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`); // Stack exists, we can continue - no need to wait for all services if (progressBar && progressText) { progressBar.style.width = '100%'; progressBar.textContent = '100%'; progressBar.classList.remove('progress-bar-animated'); progressBar.classList.add('bg-success'); progressText.textContent = 'Stack created successfully!'; } return { success: true, data: { name: stackName, id: result.data.stack_id, status: 'starting' } }; } else { console.log(`Stack ${stackName} 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...`); lastKnownStatus = 'not_found'; if (progressText) { progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } else { console.log(`Stack status check failed with status ${response.status}, continuing to poll...`); if (progressText) { progressText.textContent = `Status check failed (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } } catch (error) { console.error(`Error polling stack status (attempt ${attempts}):`, error); if (progressText) { progressText.textContent = `Error checking status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } // Wait before next poll await new Promise(resolve => setTimeout(resolve, pollInterval)); } // If we get here, we've timed out console.error(`Stack status polling timed out after ${maxWaitTime / 1000} seconds`); if (progressBar && progressText) { progressBar.style.width = '100%'; progressBar.classList.remove('progress-bar-animated'); progressBar.classList.add('bg-warning'); progressText.textContent = `Stack deployment timed out after ${Math.round(maxWaitTime / 1000)}s. Last known status: ${lastKnownStatus}`; } // Return a more informative error message const statusMessage = lastKnownStatus !== 'unknown' ? ` (last known status: ${lastKnownStatus})` : ''; return { 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, status: lastKnownStatus } }; } // Helper function to generate unique stack names with timestamp function generateStackName(port) { const now = new Date(); const timestamp = now.getFullYear().toString() + (now.getMonth() + 1).toString().padStart(2, '0') + now.getDate().toString().padStart(2, '0') + '_' + now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0') + now.getSeconds().toString().padStart(2, '0'); return `docupulse_${port}_${timestamp}`; }