Files
docupulse/static/js/launch_progress.js
2025-06-23 14:24:13 +02:00

2747 lines
116 KiB
JavaScript

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 = `
<div class="step-icon"><i class="fas fa-cloud"></i></div>
<div class="step-content">
<h5>Checking Cloudflare Connection</h5>
<p class="step-status">Verifying Cloudflare API connection...</p>
</div>
`;
stepsContainer.appendChild(cloudflareStep);
// Add DNS record creation step
const dnsCreateStep = document.createElement('div');
dnsCreateStep.className = 'step-item';
dnsCreateStep.innerHTML = `
<div class="step-icon"><i class="fas fa-plus-circle"></i></div>
<div class="step-content">
<h5>Creating DNS Records</h5>
<p class="step-status">Setting up domain DNS records in Cloudflare...</p>
</div>
`;
stepsContainer.appendChild(dnsCreateStep);
// Add DNS check step
const dnsStep = document.createElement('div');
dnsStep.className = 'step-item';
dnsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-globe"></i></div>
<div class="step-content">
<h5>Checking DNS Records</h5>
<p class="step-status">Verifying domain configurations...</p>
</div>
`;
stepsContainer.appendChild(dnsStep);
// Add NGINX connection check step
const nginxStep = document.createElement('div');
nginxStep.className = 'step-item';
nginxStep.innerHTML = `
<div class="step-icon"><i class="fas fa-network-wired"></i></div>
<div class="step-content">
<h5>Checking NGINX Connection</h5>
<p class="step-status">Verifying connection to NGINX Proxy Manager...</p>
</div>
`;
stepsContainer.appendChild(nginxStep);
// Add SSL Certificate generation step
const sslStep = document.createElement('div');
sslStep.className = 'step-item';
sslStep.innerHTML = `
<div class="step-icon"><i class="fas fa-lock"></i></div>
<div class="step-content">
<h5>Generating SSL Certificate</h5>
<p class="step-status">Setting up secure HTTPS connection...</p>
</div>
`;
stepsContainer.appendChild(sslStep);
// Add Proxy Host creation step
const proxyStep = document.createElement('div');
proxyStep.className = 'step-item';
proxyStep.innerHTML = `
<div class="step-icon"><i class="fas fa-server"></i></div>
<div class="step-content">
<h5>Creating Proxy Host</h5>
<p class="step-status">Setting up NGINX proxy host configuration...</p>
</div>
`;
stepsContainer.appendChild(proxyStep);
// Add Portainer connection check step
const portainerStep = document.createElement('div');
portainerStep.className = 'step-item';
portainerStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content">
<h5>Checking Portainer Connection</h5>
<p class="step-status">Verifying connection to Portainer...</p>
</div>
`;
stepsContainer.appendChild(portainerStep);
// Add Docker Compose download step
const dockerComposeStep = document.createElement('div');
dockerComposeStep.className = 'step-item';
dockerComposeStep.innerHTML = `
<div class="step-icon"><i class="fas fa-file-code"></i></div>
<div class="step-content">
<h5>Downloading Docker Compose</h5>
<p class="step-status">Fetching docker-compose.yml from repository...</p>
</div>
`;
stepsContainer.appendChild(dockerComposeStep);
// Add Portainer stack deployment step
const stackDeployStep = document.createElement('div');
stackDeployStep.className = 'step-item';
stackDeployStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content">
<h5>Deploying Stack</h5>
<p class="step-status">Launching your application stack...</p>
</div>
`;
stepsContainer.appendChild(stackDeployStep);
// Add Save Instance Data step
const saveDataStep = document.createElement('div');
saveDataStep.className = 'step-item';
saveDataStep.innerHTML = `
<div class="step-icon"><i class="fas fa-save"></i></div>
<div class="step-content">
<h5>Saving Instance Data</h5>
<p class="step-status">Storing instance information...</p>
</div>
`;
stepsContainer.appendChild(saveDataStep);
// Add Health Check step
const healthStep = document.createElement('div');
healthStep.className = 'step-item';
healthStep.innerHTML = `
<div class="step-icon"><i class="fas fa-heartbeat"></i></div>
<div class="step-content">
<h5>Health Check</h5>
<p class="step-status">Verifying instance health...</p>
</div>
`;
stepsContainer.appendChild(healthStep);
// Add Authentication step
const authStep = document.createElement('div');
authStep.className = 'step-item';
authStep.innerHTML = `
<div class="step-icon"><i class="fas fa-key"></i></div>
<div class="step-content">
<h5>Instance Authentication</h5>
<p class="step-status">Setting up instance authentication...</p>
</div>
`;
stepsContainer.appendChild(authStep);
// Add Apply Company Information step
const companyStep = document.createElement('div');
companyStep.className = 'step-item';
companyStep.innerHTML = `
<div class="step-icon"><i class="fas fa-building"></i></div>
<div class="step-content">
<h5>Apply Company Information</h5>
<p class="step-status">Configuring company details...</p>
</div>
`;
stepsContainer.appendChild(companyStep);
// Add Apply Colors step
const colorsStep = document.createElement('div');
colorsStep.className = 'step-item';
colorsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-palette"></i></div>
<div class="step-content">
<h5>Apply Colors</h5>
<p class="step-status">Configuring color scheme...</p>
</div>
`;
stepsContainer.appendChild(colorsStep);
// Add Update Admin Credentials step
const credentialsStep = document.createElement('div');
credentialsStep.className = 'step-item';
credentialsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-user-shield"></i></div>
<div class="step-content">
<h5>Update Admin Credentials</h5>
<p class="step-status">Setting up admin account...</p>
</div>
`;
stepsContainer.appendChild(credentialsStep);
// Add Copy SMTP Settings step
const smtpStep = document.createElement('div');
smtpStep.className = 'step-item';
smtpStep.innerHTML = `
<div class="step-icon"><i class="fas fa-envelope-open"></i></div>
<div class="step-content">
<h5>Copy SMTP Settings</h5>
<p class="step-status">Configuring email settings...</p>
</div>
`;
stepsContainer.appendChild(smtpStep);
// Add Send Completion Email step
const emailStep = document.createElement('div');
emailStep.className = 'step-item';
emailStep.innerHTML = `
<div class="step-icon"><i class="fas fa-envelope"></i></div>
<div class="step-content">
<h5>Send Completion Email</h5>
<p class="step-status">Sending notification to client...</p>
</div>
`;
stepsContainer.appendChild(emailStep);
// Add Download Launch Report step
const reportStep = document.createElement('div');
reportStep.className = 'step-item';
reportStep.innerHTML = `
<div class="step-icon"><i class="fas fa-file-download"></i></div>
<div class="step-content">
<h5>Download Launch Report</h5>
<p class="step-status">Preparing launch report...</p>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">DNS Record Creation Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Domain</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${Object.entries(dnsCreateResult.results).map(([domain, result]) => `
<tr>
<td>${domain}</td>
<td>
<span class="badge bg-${result.status === 'created' || result.status === 'updated' ? 'success' : 'danger'}">
${result.status}
</span>
</td>
<td>${result.message}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
dnsCreateStep.querySelector('.step-content').appendChild(dnsCreateDetails);
// Step 3: Check DNS records
await updateStep(3, 'Checking DNS Records', 'Verifying domain configurations...');
const dnsResult = await checkDNSRecords(data.webAddresses);
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">DNS Check Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Domain</th>
<th>Status</th>
<th>IP Address</th>
<th>TTL</th>
</tr>
</thead>
<tbody>
${Object.entries(dnsResult.results).map(([domain, result]) => `
<tr>
<td>${domain}</td>
<td>
<span class="badge bg-${result.resolved ? 'success' : 'danger'}">
${result.resolved ? 'Resolved' : 'Not Found'}
</span>
</td>
<td>${result.ip || 'N/A'}</td>
<td>${result.ttl || 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
// 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 = '<i class="fas fa-download me-1"></i> 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 = `
<div class="progress" style="height: 20px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 0%"
id="stackProgress">
0%
</div>
</div>
<small class="text-muted" id="stackProgressText">Initiating stack deployment...</small>
`;
stackDeployStepElement.querySelector('.step-content').appendChild(stackProgressDiv);
const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port);
launchReport.steps.push({
step: 'Stack Deployment',
status: stackResult.success ? 'success' : 'error',
details: stackResult
});
if (!stackResult.success) {
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Stack Deployment Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Stack Name</td>
<td>${stackResult.data.name}</td>
</tr>
<tr>
<td>Stack ID</td>
<td>${stackResult.data.id || 'Will be determined during deployment'}</td>
</tr>
<tr>
<td>Status</td>
<td>
<span class="badge bg-${stackResult.data.status === 'existing' ? 'info' : stackResult.data.status === 'active' ? 'success' : 'warning'}">
${stackResult.data.status === 'existing' ? 'Existing' : stackResult.data.status === 'active' ? 'Active' : 'Deployed'}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
stackDeployStep.querySelector('.step-content').appendChild(stackDetails);
// Save instance data
await updateStep(10, 'Saving Instance Data', 'Storing instance information...');
try {
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
};
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 = `
<div class="progress" style="height: 20px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 0%"
id="healthProgress">
0%
</div>
</div>
<small class="text-muted" id="healthProgressText">Starting health check...</small>
`;
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 = '<i class="fas fa-sync-alt me-1"></i> Retry Health Check';
retryButton.onclick = async () => {
retryButton.disabled = true;
retryButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> 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 = '<i class="fas fa-sync-alt me-1"></i> 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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Authentication Details</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Status</td>
<td><span class="badge bg-success">Authenticated</span></td>
</tr>
<tr>
<td>Default Admin</td>
<td>administrator@docupulse.com</td>
</tr>
<tr>
<td>Connection Type</td>
<td>Management API Key</td>
</tr>
<tr>
<td>Authentication Type</td>
<td>${authResult.alreadyAuthenticated ? 'Existing' : 'New'}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Company Information Applied</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Company Name</td>
<td>${data.company.name || 'Not set'}</td>
</tr>
<tr>
<td>Industry</td>
<td>${data.company.industry || 'Not set'}</td>
</tr>
<tr>
<td>Email</td>
<td>${data.company.email || 'Not set'}</td>
</tr>
<tr>
<td>Website</td>
<td>${data.company.website || 'Not set'}</td>
</tr>
<tr>
<td>Address</td>
<td>${data.company.streetAddress || 'Not set'}, ${data.company.city || ''}, ${data.company.state || ''} ${data.company.zipCode || ''}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Color Scheme Applied</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
<th>Preview</th>
</tr>
</thead>
<tbody>
<tr>
<td>Primary Color</td>
<td>${data.colors.primary || 'Not set'}</td>
<td><div style="width: 30px; height: 20px; background-color: ${data.colors.primary || '#ccc'}; border: 1px solid #ddd;"></div></td>
</tr>
<tr>
<td>Secondary Color</td>
<td>${data.colors.secondary || 'Not set'}</td>
<td><div style="width: 30px; height: 20px; background-color: ${data.colors.secondary || '#ccc'}; border: 1px solid #ddd;"></div></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">${credentialsResult.data.already_updated ? 'Admin Credentials Status' : 'Admin Credentials Updated'}</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Email Address</td>
<td>${credentialsResult.data.email || 'Not set'}</td>
</tr>
<tr>
<td>Username</td>
<td>${credentialsResult.data.username || 'administrator'}</td>
</tr>
<tr>
<td>Role</td>
<td><span class="badge bg-primary">Administrator</span></td>
</tr>
<tr>
<td>Password Setup</td>
<td><span class="badge bg-warning">Reset Link Pending</span></td>
</tr>
<tr>
<td>Status</td>
<td><span class="badge bg-${credentialsResult.data.already_updated ? 'info' : 'success'}">${credentialsResult.data.already_updated ? 'Already Updated' : 'Updated'}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-${credentialsResult.data.already_updated ? 'info' : 'warning'} mt-3">
<i class="fas fa-${credentialsResult.data.already_updated ? 'info-circle' : 'exclamation-triangle'} me-2"></i>
<strong>${credentialsResult.data.already_updated ? 'Note:' : 'Security Setup Required:'}</strong>
${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.'}
</div>
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle me-2"></i>
<strong>Login URL:</strong> <a href="https://${data.webAddresses[0]}" target="_blank">https://${data.webAddresses[0]}</a>
</div>
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">SMTP Settings Copied</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>SMTP Host</td>
<td>${smtpResult.data.smtp_host || 'Not set'}</td>
</tr>
<tr>
<td>SMTP Port</td>
<td>${smtpResult.data.smtp_port || 'Not set'}</td>
</tr>
<tr>
<td>Security</td>
<td><span class="badge bg-${smtpResult.data.smtp_security === 'ssl' ? 'success' : smtpResult.data.smtp_security === 'tls' ? 'warning' : 'secondary'}">${smtpResult.data.smtp_security || 'None'}</span></td>
</tr>
<tr>
<td>Username</td>
<td>${smtpResult.data.smtp_username || 'Not set'}</td>
</tr>
<tr>
<td>From Email</td>
<td>${smtpResult.data.smtp_from_email || 'Not set'}</td>
</tr>
<tr>
<td>From Name</td>
<td>${smtpResult.data.smtp_from_name || 'Not set'}</td>
</tr>
<tr>
<td>Status</td>
<td><span class="badge bg-success">Copied Successfully</span></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-2"></i>
<strong>Email Configuration Complete!</strong> The launched instance now has the same SMTP settings as the master instance and can send emails independently.
</div>
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Completion Email Sent</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Recipient</td>
<td>${data.company.email || 'Not set'}</td>
</tr>
<tr>
<td>Subject</td>
<td>Your DocuPulse Instance is Ready!</td>
</tr>
<tr>
<td>Status</td>
<td><span class="badge bg-success">Sent Successfully</span></td>
</tr>
<tr>
<td>Instance URL</td>
<td><a href="https://${data.webAddresses[0]}" target="_blank">https://${data.webAddresses[0]}</a></td>
</tr>
<tr>
<td>Password Reset</td>
<td><span class="badge bg-success">Link Included</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Email Content Dropdown -->
<div class="accordion mt-3" id="emailContentAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="emailContentHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#emailContentCollapse" aria-expanded="false" aria-controls="emailContentCollapse">
<i class="fas fa-envelope me-2"></i>View Sent Email Content
</button>
</h2>
<div id="emailContentCollapse" class="accordion-collapse collapse" aria-labelledby="emailContentHeader" data-bs-parent="#emailContentAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">HTML Version</h6>
<div class="border rounded p-3 bg-light" style="max-height: 400px; overflow-y: auto; font-size: 0.9em;">
<div class="email-preview">
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="background-color: #16767b; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0;">
<h1>🎉 Your DocuPulse Instance is Ready!</h1>
</div>
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 0 0 5px 5px;">
<p>Dear ${data.company.name || 'Valued Customer'},</p>
<p>Great news! Your DocuPulse instance has been successfully deployed and configured.</p>
<div style="background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 15px 0;">
<h3>📋 Instance Details</h3>
<p><strong>Instance URL:</strong> <a href="https://${data.webAddresses[0]}">https://${data.webAddresses[0]}</a></p>
<p><strong>Company Name:</strong> ${data.company.name || 'Not set'}</p>
<p><strong>Industry:</strong> ${data.company.industry || 'Not set'}</p>
<p><strong>Deployment Date:</strong> ${new Date().toLocaleString()}</p>
</div>
<div style="background-color: #e8f5e9; padding: 15px; border-radius: 5px; margin: 15px 0;">
<h3>🔐 Account Access</h3>
<p><strong>Email Address:</strong> ${data.company.email || 'Not set'}</p>
<p><strong>Username:</strong> administrator</p>
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0;">
<h4>🔒 Security Setup Required</h4>
<p>For your security, you need to set up your password. Click the button below to create your secure password.</p>
<p><strong>Password Reset Link Expires:</strong> 24 hours</p>
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="#" class="btn btn-primary" style="background-color: #16767b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0;">🔐 Set Up Your Password</a>
<br><br>
<a href="https://${data.webAddresses[0]}" class="btn btn-primary" style="background-color: #16767b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0;">🚀 Access Your Instance</a>
</div>
<h3>✅ What's Been Configured</h3>
<ul>
<li>✅ Secure SSL certificate for HTTPS access</li>
<li>✅ Company information and branding</li>
<li>✅ Custom color scheme</li>
<li>✅ Admin account created</li>
<li>✅ Document management system ready</li>
</ul>
<h3>🎯 Next Steps</h3>
<ol>
<li>Click the "Set Up Your Password" button above</li>
<li>Create your secure password</li>
<li>Return to your instance and log in</li>
<li>Explore your new DocuPulse platform</li>
<li>Start uploading and organizing your documents</li>
<li>Invite team members to collaborate</li>
</ol>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666;">
<p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p>
<p>Thank you for choosing DocuPulse!</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<h6 class="text-primary">Plain Text Version</h6>
<div class="border rounded p-3 bg-light" style="max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 0.85em; white-space: pre-wrap;">
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!
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="alert alert-success mt-3">
<i class="fas fa-check-circle me-2"></i>
<strong>Launch Complete!</strong> 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.
</div>
</div>
</div>
`;
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 = '<i class="fas fa-file-download me-1"></i> 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 = `
<div class="mt-3">
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">NGINX Proxy Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Proxy Host ID</td>
<td>${result.id || 'N/A'}</td>
</tr>
<tr>
<td>Domains</td>
<td>${domains.join(', ')}</td>
</tr>
<tr>
<td>Forward Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Host</td>
<td>192.168.68.124</td>
</tr>
<tr>
<td>Forward Port</td>
<td>${parseInt(port)}</td>
</tr>
<tr>
<td>SSL Status</td>
<td>
<span class="badge bg-success">Forced</span>
</td>
</tr>
<tr>
<td>SSL Certificate</td>
<td>
<span class="badge bg-success">Using Certificate ID: ${sslCertificateId}</span>
</td>
</tr>
<tr>
<td>Security Features</td>
<td>
<span class="badge bg-success me-1">Block Exploits</span>
<span class="badge bg-success me-1">HSTS</span>
<span class="badge bg-success">HTTP/2</span>
</td>
</tr>
<tr>
<td>Performance</td>
<td>
<span class="badge bg-success me-1">Caching</span>
<span class="badge bg-success">WebSocket</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
// 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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">SSL Certificate Details</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Certificate ID</td>
<td>${result.id || 'N/A'}</td>
</tr>
<tr>
<td>Domains</td>
<td>${(result.domain_names || domains).join(', ')}</td>
</tr>
<tr>
<td>Provider</td>
<td>${result.provider || 'Let\'s Encrypt'}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
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
};
} 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);
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 || '', // Use empty string if null
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 || '', // Use empty string if null
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 = 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 {
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 {
// 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: `docupulse_${port}`,
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()
}
]
})
});
// Handle 504 Gateway Timeout as successful initiation
if (response.status === 504) {
console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
return {
success: true,
data: {
name: stackName,
id: null, // Will be determined during polling
status: 'creating'
}
};
}
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 = 10 * 60 * 1000) {
const startTime = Date.now();
const pollInterval = 5000; // 5 seconds
let attempts = 0;
console.log(`Starting to poll stack status for: ${stackName}`);
// 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
const elapsed = Date.now() - startTime;
const progress = Math.min((elapsed / maxWaitTime) * 100, 95); // Cap at 95% until complete
if (progressBar && progressText) {
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${Math.round(progress)}%`;
progressText.textContent = `Checking stack status (attempt ${attempts})...`;
}
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();
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%';
progressText.textContent = 'Stack is now active!';
}
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...`);
if (progressText) {
progressText.textContent = `Stack is partially running (attempt ${attempts})...`;
}
} else if (result.data && result.data.status === 'inactive') {
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
if (progressText) {
progressText.textContent = `Stack is starting up (attempt ${attempts})...`;
}
} else {
console.log(`Stack ${stackName} status unknown, continuing to poll...`);
if (progressText) {
progressText.textContent = `Checking stack status (attempt ${attempts})...`;
}
}
} else if (response.status === 404) {
console.log(`Stack ${stackName} not found yet, continuing to poll...`);
if (progressText) {
progressText.textContent = `Stack not found yet (attempt ${attempts})...`;
}
} else {
console.log(`Stack status check failed with status ${response.status}, continuing to poll...`);
if (progressText) {
progressText.textContent = `Status check failed (attempt ${attempts})...`;
}
}
} catch (error) {
console.error(`Error polling stack status (attempt ${attempts}):`, error);
if (progressText) {
progressText.textContent = `Error checking status (attempt ${attempts})...`;
}
}
// 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';
}
return {
success: false,
error: `Stack deployment timed out after ${maxWaitTime / 1000} seconds. The stack may still be deploying.`
};
}