/**
* Launch Progress JavaScript
*
* This file handles the instance launch and update process, including:
* - Step-by-step progress tracking
* - Stack deployment via Portainer API
* - Error handling for HTTP 502/504 responses
*
* Note: HTTP 502 (Bad Gateway) and 504 (Gateway Timeout) errors are treated as
* potential success cases since they often occur when Portainer is busy but the
* operation may still succeed. In these cases, the system continues monitoring
* the stack status.
*/
document.addEventListener('DOMContentLoaded', function() {
// Check if this is an update operation
if (window.isUpdate && window.updateInstanceId && window.updateRepoId && window.updateBranch) {
// This is an update operation
const updateData = {
instanceId: window.updateInstanceId,
repository: window.updateRepoId,
branch: window.updateBranch,
isUpdate: true
};
// Initialize the steps
initializeSteps();
// Start the update process
startUpdate(updateData);
} else {
// This is a new launch operation
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');
const isUpdate = window.isUpdate;
if (isUpdate) {
// For updates, show fewer steps
const steps = [
{ icon: 'fab fa-docker', title: 'Checking Portainer Connection', description: 'Verifying connection to Portainer...' },
{ icon: 'fas fa-file-code', title: 'Downloading Docker Compose', description: 'Fetching docker-compose.yml from repository...' },
{ icon: 'fab fa-docker', title: 'Deploying Updated Stack', description: 'Deploying the updated application stack...' },
{ icon: 'fas fa-save', title: 'Updating Instance Data', description: 'Updating instance information...' },
{ icon: 'fas fa-heartbeat', title: 'Health Check', description: 'Verifying updated instance health...' },
{ icon: 'fas fa-check-circle', title: 'Update Complete', description: 'Instance has been successfully updated!' }
];
steps.forEach((step, index) => {
const stepElement = document.createElement('div');
stepElement.className = 'step-item';
stepElement.innerHTML = `
${step.title}
${step.description}
`;
stepsContainer.appendChild(stepElement);
});
} else {
// For new launches, show all steps
// 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 completion notification...
`;
stepsContainer.appendChild(emailStep);
// Add Download 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
| Domain |
Status |
Message |
${Object.entries(dnsCreateResult.results).map(([domain, result]) => `
| ${domain} |
${result.status}
|
${result.message} |
`).join('')}
`;
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
| Domain |
Status |
IP Address |
TTL |
${Object.entries(dnsResult.results).map(([domain, result]) => `
| ${domain} |
${result.resolved ? 'Resolved' : 'Not Found'}
|
${result.ip || 'N/A'} |
${result.ttl || 'N/A'} |
`).join('')}
`;
// 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 = () => {
// Generate the modified docker-compose content with updated volume names
let modifiedContent = dockerComposeResult.content;
const stackName = generateStackName(data.port);
// Extract timestamp from stack name (format: docupulse_PORT_TIMESTAMP)
const stackNameParts = stackName.split('_');
if (stackNameParts.length >= 3) {
const timestamp = stackNameParts.slice(2).join('_'); // Get everything after port
const baseName = `docupulse_${data.port}_${timestamp}`;
// Replace volume names to match stack naming convention
modifiedContent = modifiedContent.replace(
/name: docupulse_\$\{PORT:-10335\}_postgres_data/g,
`name: ${baseName}_postgres_data`
);
modifiedContent = modifiedContent.replace(
/name: docupulse_\$\{PORT:-10335\}_uploads/g,
`name: ${baseName}_uploads`
);
}
const blob = new Blob([modifiedContent], { 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 = `
Initiating stack deployment...
Note: Stack deployment can take several minutes. If you see HTTP 502 or 504 errors,
the deployment may still be in progress and will be monitored automatically.
`;
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') ||
stackResult.error.includes('504 Gateway Time-out') ||
stackResult.error.includes('504 Gateway Timeout')
)) {
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';
// Calculate the volume names based on the stack name
const stackNameParts = stackResult.data.name.split('_');
let volumeNames = [];
if (stackNameParts.length >= 3) {
const timestamp = stackNameParts.slice(2).join('_');
const baseName = `docupulse_${data.port}_${timestamp}`;
volumeNames = [
`${baseName}_postgres_data`,
`${baseName}_uploads`
];
}
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'}
|
| Volume Names |
${volumeNames.length > 0 ? volumeNames.map(name =>
`${name}`
).join(' ') : 'Using default volume names'}
|
${volumeNames.length > 0 ? `
Volume Naming Convention: Volumes have been named using the same timestamp as the stack for easy identification and management.
` : ''}
`;
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 = `
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
🎉 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. Click the button below to create your secure password.
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
- Click the "Set Up Your Password" button above
- Create your secure password
- Return to your instance and log in
- Explore your new DocuPulse platform
- Start uploading and organizing your documents
- 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!
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);
}
}
// Function to handle instance updates
async function startUpdate(data) {
console.log('Starting instance update:', data);
try {
// Update the header to reflect this is an update
const headerTitle = document.querySelector('.header h1');
const headerDescription = document.querySelector('.header p');
if (headerTitle) headerTitle.textContent = 'Updating Instance';
if (headerDescription) headerDescription.textContent = 'Updating your DocuPulse instance with the latest version';
// Initialize launch report for update
const launchReport = {
type: 'update',
timestamp: new Date().toISOString(),
instanceId: data.instanceId,
repository: data.repository,
branch: data.branch,
steps: []
};
// Step 1: Check Portainer Connection
await updateStep(1, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
const portainerResult = await checkPortainerConnection();
if (!portainerResult.success) {
throw new Error(`Portainer connection failed: ${portainerResult.error}`);
}
launchReport.steps.push({
step: 'Portainer Connection',
status: 'success',
details: portainerResult
});
// Step 2: Download Docker Compose
await updateStep(2, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
if (!dockerComposeResult.success) {
throw new Error(`Failed to download Docker Compose: ${dockerComposeResult.error}`);
}
launchReport.steps.push({
step: 'Docker Compose Download',
status: 'success',
details: dockerComposeResult
});
// Step 3: Deploy Updated Stack
await updateStep(3, 'Deploying Updated Stack', 'Deploying the updated application stack...');
// Get the existing instance information to extract port and stack details
const instanceResponse = await fetch(`/api/instances/${data.instanceId}`);
if (!instanceResponse.ok) {
throw new Error('Failed to get instance information');
}
const instanceData = await instanceResponse.json();
const port = instanceData.instance.name; // Assuming the instance name is the port
// Check if we have an existing stack ID
if (!instanceData.instance.portainer_stack_id) {
throw new Error('No existing stack found for this instance. Cannot perform update.');
}
// Update the existing stack instead of creating a new one
const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port, instanceData.instance);
if (!stackResult.success) {
throw new Error(`Failed to update stack: ${stackResult.error}`);
}
launchReport.steps.push({
step: 'Stack Update',
status: 'success',
details: stackResult
});
// Step 4: Update Instance Data
await updateStep(4, 'Updating Instance Data', 'Updating instance information...');
const updateData = {
name: instanceData.instance.name,
port: port,
domains: instanceData.instance.main_url ? [instanceData.instance.main_url.replace(/^https?:\/\//, '')] : [],
stack_id: instanceData.instance.portainer_stack_id, // Keep the same stack ID
stack_name: instanceData.instance.portainer_stack_name, // Keep the same stack name
status: stackResult.data.status,
repository: data.repository,
branch: data.branch,
deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown',
deployed_branch: data.branch,
payment_plan: instanceData.instance.payment_plan || 'Basic'
};
const saveResult = await saveInstanceData(updateData);
if (!saveResult.success) {
throw new Error(`Failed to update instance data: ${saveResult.error}`);
}
launchReport.steps.push({
step: 'Instance Data Update',
status: 'success',
details: saveResult
});
// Step 5: Health Check
await updateStep(5, 'Health Check', 'Verifying updated instance health...');
const healthResult = await checkInstanceHealth(instanceData.instance.main_url);
if (!healthResult.success) {
throw new Error(`Health check failed: ${healthResult.error}`);
}
launchReport.steps.push({
step: 'Health Check',
status: 'success',
details: healthResult
});
// Update completed successfully
await updateStep(6, 'Update Complete', 'Instance has been successfully updated!');
// Show success message
const successStep = document.querySelectorAll('.step-item')[5];
successStep.classList.remove('active');
successStep.classList.add('completed');
successStep.querySelector('.step-status').textContent = 'Instance updated successfully!';
// Add success details
const successDetails = document.createElement('div');
successDetails.className = 'mt-3';
// Calculate the volume names based on the stack name
const stackNameParts = stackResult.data.name.split('_');
let volumeNames = [];
if (stackNameParts.length >= 3) {
const timestamp = stackNameParts.slice(2).join('_');
const baseName = `docupulse_${port}_${timestamp}`;
volumeNames = [
`${baseName}_postgres_data`,
`${baseName}_uploads`
];
}
successDetails.innerHTML = `
Update Completed Successfully!
Your instance has been updated with the latest version from the repository. All existing data and volumes have been preserved.
Repository: ${data.repository}
Branch: ${data.branch}
New Version: ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'}
Data Preservation: All existing data, volumes, and configurations have been preserved during this update.
`;
successStep.querySelector('.step-content').appendChild(successDetails);
// Add button to return to instances page
const returnButton = document.createElement('button');
returnButton.className = 'btn btn-primary mt-3';
returnButton.innerHTML = 'Return to Instances';
returnButton.onclick = () => window.location.href = '/instances';
successStep.querySelector('.step-content').appendChild(returnButton);
// Store the update report
sessionStorage.setItem('instanceUpdateReport', JSON.stringify(launchReport));
} catch (error) {
console.error('Update failed:', error);
await updateStep(6, 'Update Failed', `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 - check if we're in update mode or launch mode
const isUpdate = window.isUpdate;
const healthStepIndex = isUpdate ? 4 : 10; // Different indices for update vs launch
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
if (healthStepElement) {
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% if it exists
const progressBar = document.getElementById('healthProgress');
const progressText = document.getElementById('healthProgressText');
if (progressBar && progressText) {
progressBar.style.width = '100%';
progressBar.textContent = '100%';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success');
progressText.textContent = `Health check completed successfully in ${elapsedTime}s`;
}
return {
success: true,
data: data,
attempts: currentAttempt,
elapsedTime: elapsedTime
};
} else if (data.status === 'inactive') {
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
lastKnownStatus = 'inactive';
if (progressText) {
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
} else if (data.status === 'starting') {
console.log(`Stack ${stackName} is starting up, continuing to poll...`);
lastKnownStatus = 'starting';
if (progressText) {
progressText.textContent = `Stack is initializing (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
} else {
throw new Error('Instance is not healthy');
}
} else {
// If step element doesn't exist, just log the status
console.log(`Health check - Instance status: ${data.status}`);
if (data.status === 'active') {
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
return {
success: true,
data: data,
attempts: currentAttempt,
elapsedTime: elapsedTime
};
}
}
} catch (error) {
console.error(`Health check attempt ${currentAttempt} failed:`, error);
// Update status to show current attempt and elapsed time - check if step element exists
const isUpdate = window.isUpdate;
const healthStepIndex = isUpdate ? 4 : 10;
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
if (healthStepElement) {
const statusText = healthStepElement.querySelector('.step-status');
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed): ${error.message}`;
// Update progress bar
const progressBar = document.getElementById('healthProgress');
const progressText = document.getElementById('healthProgressText');
if (progressBar && progressText) {
const progressPercent = Math.min((currentAttempt / maxRetries) * 100, 100);
progressBar.style.width = `${progressPercent}%`;
progressBar.textContent = `${Math.round(progressPercent)}%`;
progressText.textContent = `Attempt ${currentAttempt}/${maxRetries} (${elapsedTime}s elapsed)`;
}
}
if (currentAttempt === maxRetries || (Date.now() - startTime > maxTotalTime)) {
// Update progress bar to show failure if it exists
const progressBar = document.getElementById('healthProgress');
const progressText = document.getElementById('healthProgressText');
if (progressBar && progressText) {
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-danger');
progressText.textContent = `Health check failed after ${currentAttempt} attempts (${elapsedTime}s)`;
}
return {
success: false,
error: `Health check failed after ${currentAttempt} attempts (${Math.round((Date.now() - startTime) / 1000)}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 if it exists
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
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 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);
}
}
// Update volume names in docker-compose content to match stack naming convention
let modifiedDockerComposeContent = dockerComposeContent;
// Extract timestamp from stack name (format: docupulse_PORT_TIMESTAMP)
const stackNameParts = stackName.split('_');
if (stackNameParts.length >= 3) {
const timestamp = stackNameParts.slice(2).join('_'); // Get everything after port
const baseName = `docupulse_${port}_${timestamp}`;
// Replace volume names to match stack naming convention
modifiedDockerComposeContent = modifiedDockerComposeContent.replace(
/name: docupulse_\$\{PORT:-10335\}_postgres_data/g,
`name: ${baseName}_postgres_data`
);
modifiedDockerComposeContent = modifiedDockerComposeContent.replace(
/name: docupulse_\$\{PORT:-10335\}_uploads/g,
`name: ${baseName}_uploads`
);
console.log(`Updated volume names to match stack naming convention: ${baseName}`);
}
// First, attempt to deploy the stack
console.log('Making stack deployment request to /api/admin/deploy-stack');
console.log('Stack name:', stackName);
console.log('Port:', port);
console.log('Modified docker-compose content length:', modifiedDockerComposeContent.length);
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: modifiedDockerComposeContent,
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'
}
]
})
});
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
// Log additional response details for debugging
if (response.status === 502) {
console.log('HTTP 502 Bad Gateway detected - this usually means Portainer is busy or slow to respond');
console.log('The stack deployment may still be in progress despite this error');
} else if (response.status === 504) {
console.log('HTTP 504 Gateway Timeout detected - the request took too long');
console.log('This is expected for long-running stack deployments');
}
// 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('launchProgress');
const progressText = document.getElementById('stepDescription');
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) {
let errorMessage = 'Failed to deploy stack';
console.log('Response not ok, status:', response.status);
// Handle 502 Bad Gateway and 504 Gateway Timeout as potential success cases
// These errors often occur when Portainer is busy or slow to respond, but the operation may still succeed
if (response.status === 502 || response.status === 504) {
console.log(`Received HTTP ${response.status} - stack creation may still be in progress`);
console.log('HTTP 502 (Bad Gateway) typically means Portainer is busy or slow to respond');
console.log('HTTP 504 (Gateway Timeout) means the request took too long');
console.log('In both cases, we continue monitoring since the operation may still succeed');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = `Stack creation initiated (HTTP ${response.status}, but continuing to monitor)...`;
}
// Start polling immediately since the stack creation was initiated
console.log(`Starting to poll for stack status after HTTP ${response.status}...`);
const pollResult = await pollStackStatus(stackName, 15 * 60 * 1000); // 15 minutes max
return pollResult;
}
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
console.log('Parsed error data:', errorData);
} catch (parseError) {
console.log('Failed to parse JSON error, trying text:', parseError);
// If JSON parsing fails, try to get text content
try {
const errorText = await response.text();
console.log('Error text content:', errorText);
if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) {
console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = 'Stack 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;
} else {
errorMessage = `HTTP ${response.status}: ${errorText}`;
}
} catch (textError) {
console.log('Failed to get error text:', textError);
errorMessage = `HTTP ${response.status}: Failed to parse response`;
}
}
console.log('Throwing error:', errorMessage);
throw new Error(errorMessage);
}
const result = await response.json();
console.log('Stack 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);
// Check if this is a 502 or 504 error that should be handled as a success
if (error.message && (
error.message.includes('504 Gateway Time-out') ||
error.message.includes('504 Gateway Timeout') ||
error.message.includes('timed out') ||
error.message.includes('502') ||
error.message.includes('Bad Gateway')
)) {
console.log('Detected HTTP 502/504 error in catch block - treating as successful initiation');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = 'Stack creation initiated (HTTP error, but continuing to monitor)...';
}
// Start polling immediately since the stack creation was initiated
console.log('Starting to poll for stack status after HTTP 502/504 error from catch block...');
const pollResult = await pollStackStatus(stackName, 15 * 60 * 1000); // 15 minutes max
return pollResult;
}
return {
success: false,
error: error.message
};
}
}
// Function to poll stack status
async function pollStackStatus(stackIdentifier, maxWaitTime = 15 * 60 * 1000) {
const startTime = Date.now();
const pollInterval = 5000; // 5 seconds
let attempts = 0;
let lastKnownStatus = 'unknown';
// Validate stack identifier (can be name or ID)
if (!stackIdentifier || typeof stackIdentifier !== 'string') {
console.error('Invalid stack identifier provided to pollStackStatus:', stackIdentifier);
return {
success: false,
error: `Invalid stack identifier: ${stackIdentifier}`,
data: {
name: stackIdentifier,
status: 'error'
}
};
}
console.log(`Starting to poll stack status for: ${stackIdentifier} (max wait: ${maxWaitTime / 1000}s)`);
// Update progress indicator
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
while (Date.now() - startTime < maxWaitTime) {
attempts++;
console.log(`Polling attempt ${attempts} for stack: ${stackIdentifier}`);
// Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0%
const elapsed = Date.now() - startTime;
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 {
// Determine if this is a stack ID (numeric) or stack name
const isStackId = /^\d+$/.test(stackIdentifier);
const requestBody = isStackId ?
{ stack_id: stackIdentifier } :
{ stack_name: stackIdentifier };
console.log(`Sending stack status check request:`, requestBody);
const response = await fetch('/api/admin/check-stack-status', {
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 ${stackIdentifier} 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: result.data.name || stackIdentifier,
id: result.data.stack_id || stackIdentifier,
status: 'active'
}
};
} else if (result.data && result.data.status === 'partial') {
console.log(`Stack ${stackIdentifier} is partially running, continuing to poll...`);
lastKnownStatus = 'partial';
if (progressText) {
progressText.textContent = `Stack is partially running (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
} else if (result.data && result.data.status === 'inactive') {
console.log(`Stack ${stackIdentifier} is inactive, continuing to poll...`);
lastKnownStatus = 'inactive';
if (progressText) {
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
} else if (result.data && result.data.status === 'starting') {
console.log(`Stack ${stackIdentifier} exists and is starting up - continuing to next step`);
// Stack exists, we can continue - no need to wait for all services
if (progressBar && progressText) {
progressBar.style.width = '100%';
progressBar.textContent = '100%';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success');
progressText.textContent = 'Stack created successfully!';
}
return {
success: true,
data: {
name: result.data.name || stackIdentifier,
id: result.data.stack_id || stackIdentifier,
status: 'starting'
}
};
} else {
console.log(`Stack ${stackIdentifier} status unknown, continuing to poll...`);
lastKnownStatus = 'unknown';
if (progressText) {
progressText.textContent = `Checking stack status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
}
} else if (response.status === 404) {
console.log(`Stack ${stackIdentifier} not found yet, continuing to poll...`);
lastKnownStatus = 'not_found';
if (progressText) {
progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
}
} 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: stackIdentifier,
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}`;
}
// Add new function to update existing stack
async function updateStack(dockerComposeContent, stackId, port, instanceData = null) {
try {
console.log('Updating existing stack:', stackId);
console.log('Port:', port);
console.log('Modified docker-compose content length:', dockerComposeContent.length);
// For updates, we only need to update version-related environment variables
// All other environment variables (pricing tiers, quotas, etc.) should be preserved
// We also preserve the existing docker-compose configuration
const envVars = [
{
name: 'APP_VERSION',
value: window.currentDeploymentVersion || 'unknown'
},
{
name: 'GIT_COMMIT',
value: window.currentDeploymentCommit || 'unknown'
},
{
name: 'GIT_BRANCH',
value: window.currentDeploymentBranch || 'unknown'
},
{
name: 'DEPLOYED_AT',
value: new Date().toISOString()
}
];
console.log('Updating stack with version environment variables:', envVars);
console.log('Preserving existing docker-compose configuration');
const response = await fetch('/api/admin/update-stack', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
stack_id: stackId,
// Don't send StackFileContent during updates - preserve existing configuration
Env: envVars
})
});
console.log('Update response status:', response.status);
console.log('Update response ok:', response.ok);
console.log('Update response headers:', Object.fromEntries(response.headers.entries()));
// Handle 504 Gateway Timeout as successful initiation
if (response.status === 504) {
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
}
// Start polling immediately since the stack update was initiated
console.log('Starting to poll for stack status after 504 timeout...');
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
return pollResult;
}
if (!response.ok) {
let errorMessage = 'Failed to update stack';
console.log('Response not ok, status:', response.status);
// Handle 502 Bad Gateway and 504 Gateway Timeout as potential success cases
if (response.status === 502 || response.status === 504) {
console.log(`Received HTTP ${response.status} - stack update may still be in progress`);
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = `Stack update initiated (HTTP ${response.status}, but continuing to monitor)...`;
}
// Start polling immediately since the stack update was initiated
console.log(`Starting to poll for stack status after HTTP ${response.status}...`);
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
return pollResult;
}
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
console.log('Parsed error data:', errorData);
} catch (parseError) {
console.log('Failed to parse JSON error, trying text:', parseError);
// If JSON parsing fails, try to get text content
try {
const errorText = await response.text();
console.log('Error text content:', errorText);
if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) {
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
}
// Start polling immediately since the stack update was initiated
console.log('Starting to poll for stack status after 504 timeout...');
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
return pollResult;
} else {
errorMessage = `HTTP ${response.status}: ${errorText}`;
}
} catch (textError) {
console.log('Failed to get error text:', textError);
errorMessage = `HTTP ${response.status}: Failed to parse response`;
}
}
console.log('Throwing error:', errorMessage);
throw new Error(errorMessage);
}
const result = await response.json();
console.log('Stack update initiated:', result);
// If stack is being updated, poll for status
if (result.data.status === 'updating') {
console.log('Stack is being updated, polling for status...');
const pollResult = await pollStackStatus(stackId, 10 * 60 * 1000); // 10 minutes max
return pollResult;
}
// Return success result with response data
return {
success: true,
data: result.data
};
} catch (error) {
console.error('Error updating stack:', error);
// Check if this is a 502 or 504 error that should be handled as a success
if (error.message && (
error.message.includes('504 Gateway Time-out') ||
error.message.includes('504 Gateway Timeout') ||
error.message.includes('timed out') ||
error.message.includes('502') ||
error.message.includes('Bad Gateway')
)) {
console.log('Detected HTTP 502/504 error in catch block - treating as successful initiation');
// Update progress to show that we're now polling
const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) {
progressBar.style.width = '25%';
progressBar.textContent = '25%';
progressText.textContent = 'Stack update initiated (HTTP error, but continuing to monitor)...';
}
// Start polling immediately since the stack update was initiated
console.log('Starting to poll for stack status after HTTP 502/504 error from catch block...');
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
return pollResult;
}
return {
success: false,
error: error.message
};
}
}
async function authenticateInstance(instanceUrl, instanceId) {
try {
// First check if instance is already authenticated
const instancesResponse = await fetch('/instances');
const text = await instancesResponse.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Find the instance with matching URL
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
return urlCell && urlCell.textContent.trim() === instanceUrl;
});
if (!instanceRow) {
throw new Error('Instance not found in database');
}
// Get the instance ID from the status badge's data attribute
const statusBadge = instanceRow.querySelector('[data-instance-id]');
if (!statusBadge) {
throw new Error('Could not find instance ID');
}
const dbInstanceId = statusBadge.dataset.instanceId;
// Check if already authenticated
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
if (!authStatusResponse.ok) {
throw new Error('Failed to check authentication status');
}
const authStatus = await authStatusResponse.json();
if (authStatus.authenticated) {
console.log('Instance is already authenticated');
return {
success: true,
message: 'Instance is already authenticated',
alreadyAuthenticated: true
};
}
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
// First login to get token
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: 'administrator@docupulse.com',
password: 'changeme'
})
});
if (!loginResponse.ok) {
const errorText = await loginResponse.text();
throw new Error(`Login failed: ${errorText}`);
}
const loginData = await loginResponse.json();
if (loginData.status !== 'success' || !loginData.token) {
throw new Error('Login failed: Invalid response from server');
}
const token = loginData.token;
// Then create management API key
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
name: `Connection from ${window.location.hostname}`
})
});
if (!keyResponse.ok) {
const errorText = await keyResponse.text();
throw new Error(`Failed to create API key: ${errorText}`);
}
const keyData = await keyResponse.json();
if (!keyData.api_key) {
throw new Error('No API key received from server');
}
// Save the token to our database
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ token: keyData.api_key })
});
if (!saveResponse.ok) {
const errorText = await saveResponse.text();
throw new Error(`Failed to save token: ${errorText}`);
}
return {
success: true,
message: 'Successfully authenticated instance',
alreadyAuthenticated: false
};
} catch (error) {
console.error('Authentication error:', error);
return {
success: false,
error: error.message
};
}
}