diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 23b05ba..91be6ce 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/main.py b/routes/main.py index 1148f51..6fbb3fd 100644 --- a/routes/main.py +++ b/routes/main.py @@ -774,6 +774,32 @@ def init_routes(main_bp): return render_template('main/instance_detail.html', instance=instance) + @main_bp.route('/api/instances/') + @login_required + @require_password_change + def get_instance_data(instance_id): + if not os.environ.get('MASTER', 'false').lower() == 'true': + return jsonify({'error': 'Unauthorized'}), 403 + + instance = Instance.query.get_or_404(instance_id) + + return jsonify({ + 'success': True, + 'instance': { + 'id': instance.id, + 'name': instance.name, + 'company': instance.company, + 'main_url': instance.main_url, + 'status': instance.status, + 'payment_plan': instance.payment_plan, + 'portainer_stack_id': instance.portainer_stack_id, + 'portainer_stack_name': instance.portainer_stack_name, + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch, + 'connection_token': instance.connection_token + } + }) + @main_bp.route('/instances//auth-status') @login_required @require_password_change @@ -2139,6 +2165,12 @@ def init_routes(main_bp): flash('This page is only available in master instances.', 'error') return redirect(url_for('main.dashboard')) + # Get update parameters if this is an update operation + is_update = request.args.get('update', 'false').lower() == 'true' + instance_id = request.args.get('instance_id') + repo_id = request.args.get('repo') + branch = request.args.get('branch') + # Get NGINX settings nginx_settings = KeyValueSettings.get_value('nginx_settings') # Get Portainer settings @@ -2149,7 +2181,11 @@ def init_routes(main_bp): return render_template('main/launch_progress.html', nginx_settings=nginx_settings, portainer_settings=portainer_settings, - cloudflare_settings=cloudflare_settings) + cloudflare_settings=cloudflare_settings, + is_update=is_update, + instance_id=instance_id, + repo_id=repo_id, + branch=branch) @main_bp.route('/api/check-dns', methods=['POST']) @login_required diff --git a/static/js/instances.js b/static/js/instances.js index 880a8e8..f530291 100644 --- a/static/js/instances.js +++ b/static/js/instances.js @@ -4,6 +4,7 @@ let editInstanceModal; let addExistingInstanceModal; let authModal; let launchStepsModal; +let updateInstanceModal; let currentStep = 1; // Update the total number of steps @@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', function() { addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal')); authModal = new bootstrap.Modal(document.getElementById('authModal')); launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal')); + updateInstanceModal = new bootstrap.Modal(document.getElementById('updateInstanceModal')); // Initialize tooltips const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); @@ -1774,4 +1776,168 @@ async function confirmDeleteInstance() { confirmDeleteBtn.className = 'btn btn-danger'; }, 3000); } -} \ No newline at end of file +} + +// Update Instance Functions +function showUpdateInstanceModal(instanceId, stackName, instanceUrl) { + document.getElementById('update_instance_id').value = instanceId; + document.getElementById('update_stack_name').value = stackName; + document.getElementById('update_instance_url').value = instanceUrl; + + // Load repositories for the update modal + loadUpdateRepositories(); + + updateInstanceModal.show(); +} + +async function loadUpdateRepositories() { + const repoSelect = document.getElementById('updateRepoSelect'); + const branchSelect = document.getElementById('updateBranchSelect'); + + try { + // Reset branch select + branchSelect.innerHTML = ''; + branchSelect.disabled = true; + + const gitSettings = window.gitSettings || {}; + if (!gitSettings.url || !gitSettings.token) { + throw new Error('No Git settings found. Please configure Git in the settings page.'); + } + + // Load repositories using the correct existing endpoint + const repoResponse = await fetch('/api/admin/list-gitea-repos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + url: gitSettings.url, + token: gitSettings.token + }) + }); + + if (!repoResponse.ok) { + throw new Error('Failed to load repositories'); + } + + const data = await repoResponse.json(); + + if (data.repositories && data.repositories.length > 0) { + repoSelect.innerHTML = '' + + data.repositories.map(repo => + `` + ).join(''); + repoSelect.disabled = false; + + // If we have a saved repository, load its branches + if (gitSettings.repo) { + loadUpdateBranches(gitSettings.repo); + } + } else { + repoSelect.innerHTML = ''; + repoSelect.disabled = true; + } + } catch (error) { + console.error('Error loading repositories for update:', error); + repoSelect.innerHTML = ``; + repoSelect.disabled = true; + } +} + +async function loadUpdateBranches(repoId) { + const branchSelect = document.getElementById('updateBranchSelect'); + + if (!repoId) { + branchSelect.innerHTML = ''; + branchSelect.disabled = true; + return; + } + + try { + const gitSettings = window.gitSettings || {}; + if (!gitSettings.url || !gitSettings.token) { + throw new Error('No Git settings found. Please configure Git in the settings page.'); + } + + const response = await fetch('/api/admin/list-gitea-branches', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + url: gitSettings.url, + token: gitSettings.token, + repo: repoId + }) + }); + + if (!response.ok) { + throw new Error('Failed to load branches'); + } + + const data = await response.json(); + + if (data.branches && data.branches.length > 0) { + branchSelect.innerHTML = '' + + data.branches.map(branch => + `` + ).join(''); + branchSelect.disabled = false; + } else { + branchSelect.innerHTML = ''; + branchSelect.disabled = true; + } + } catch (error) { + console.error('Error loading branches for update:', error); + branchSelect.innerHTML = ``; + branchSelect.disabled = true; + } +} + +async function startInstanceUpdate() { + const instanceId = document.getElementById('update_instance_id').value; + const stackName = document.getElementById('update_stack_name').value; + const instanceUrl = document.getElementById('update_instance_url').value; + const repoId = document.getElementById('updateRepoSelect').value; + const branch = document.getElementById('updateBranchSelect').value; + + if (!repoId || !branch) { + alert('Please select both a repository and a branch.'); + return; + } + + try { + // Store update data in sessionStorage for the launch progress page + const updateData = { + instanceId: instanceId, + stackName: stackName, + instanceUrl: instanceUrl, + repository: repoId, + branch: branch, + isUpdate: true + }; + sessionStorage.setItem('instanceUpdateData', JSON.stringify(updateData)); + + // Close the modal + updateInstanceModal.hide(); + + // Redirect to launch progress page with update parameters + window.location.href = `/instances/launch-progress?update=true&instance_id=${instanceId}&repo=${repoId}&branch=${encodeURIComponent(branch)}`; + + } catch (error) { + console.error('Error starting instance update:', error); + alert('Error starting update: ' + error.message); + } +} + +// Add event listeners for update modal +document.addEventListener('DOMContentLoaded', function() { + const updateRepoSelect = document.getElementById('updateRepoSelect'); + if (updateRepoSelect) { + updateRepoSelect.addEventListener('change', function() { + loadUpdateBranches(this.value); + }); + } +}); \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 01e17b5..0ed7b84 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -1,236 +1,280 @@ document.addEventListener('DOMContentLoaded', function() { - // Get the launch data from sessionStorage - const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); - if (!launchData) { - showError('No launch data found. Please start over.'); - return; - } + // 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); + // Initialize the steps + initializeSteps(); + + // Start the launch process + startLaunch(launchData); + } }); function initializeSteps() { const stepsContainer = document.getElementById('stepsContainer'); + const isUpdate = window.isUpdate; - // 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); + 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Copy SMTP Settings step + const smtpStep = document.createElement('div'); + smtpStep.className = 'step-item'; + smtpStep.innerHTML = ` +
+
+
Copy SMTP Settings
+

Configuring email settings...

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

Sending notification to client...

-
- `; - stepsContainer.appendChild(emailStep); + // Add 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 Launch Report step - const reportStep = document.createElement('div'); - reportStep.className = 'step-item'; - reportStep.innerHTML = ` -
-
-
Download Launch Report
-

Preparing launch report...

-
- `; - stepsContainer.appendChild(reportStep); + // 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) { @@ -467,7 +511,28 @@ async function startLaunch(data) { downloadButton.className = 'btn btn-sm btn-primary mt-2'; downloadButton.innerHTML = ' Download docker-compose.yml'; downloadButton.onclick = () => { - const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' }); + // 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; @@ -554,6 +619,19 @@ async function startLaunch(data) { // 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 = `
@@ -583,9 +661,25 @@ async function startLaunch(data) { + + 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. +
+ ` : ''}
`; @@ -1249,6 +1343,182 @@ Thank you for choosing DocuPulse! } } +// 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 + 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 + + // Generate new stack name with timestamp + const newStackName = generateStackName(port); + + const stackResult = await deployStack(dockerComposeResult.content, newStackName, port); + if (!stackResult.success) { + throw new Error(`Failed to deploy updated stack: ${stackResult.error}`); + } + launchReport.steps.push({ + step: 'Stack Deployment', + 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: stackResult.data.id || null, + stack_name: newStackName, + 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 = newStackName.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.

+
+
+ Repository: ${data.repository}
+ Branch: ${data.branch}
+ New Version: ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'} +
+
+ New Stack Name: ${newStackName}
+ Instance URL: ${instanceData.instance.main_url} +
+
+ ${volumeNames.length > 0 ? ` +
+ New Volume Names: +
+ ${volumeNames.map(name => `${name}`).join('
')} +
+
+ ` : ''} +
+ `; + 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 @@ -2645,6 +2915,28 @@ async function deployStack(dockerComposeContent, stackName, port) { } } + // 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 const response = await fetch('/api/admin/deploy-stack', { method: 'POST', @@ -2654,7 +2946,7 @@ async function deployStack(dockerComposeContent, stackName, port) { }, body: JSON.stringify({ name: stackName, - StackFileContent: dockerComposeContent, + StackFileContent: modifiedDockerComposeContent, Env: [ { name: 'PORT', @@ -2718,8 +3010,8 @@ async function deployStack(dockerComposeContent, stackName, port) { console.log('Received 504 Gateway Timeout - stack creation may still be in progress'); // Update progress to show that we're now polling - const progressBar = document.getElementById('stackProgress'); - const progressText = document.getElementById('stackProgressText'); + const progressBar = document.getElementById('launchProgress'); + const progressText = document.getElementById('stepDescription'); if (progressBar && progressText) { progressBar.style.width = '25%'; progressBar.textContent = '25%'; @@ -2733,8 +3025,38 @@ async function deployStack(dockerComposeContent, stackName, port) { } if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to deploy stack'); + let errorMessage = 'Failed to deploy stack'; + try { + const errorData = await response.json(); + errorMessage = errorData.error || errorMessage; + } catch (parseError) { + // If JSON parsing fails, try to get text content + try { + const errorText = await response.text(); + 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) { + errorMessage = `HTTP ${response.status}: Failed to parse response`; + } + } + throw new Error(errorMessage); } const result = await response.json(); @@ -2785,8 +3107,8 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`); // Update progress indicator - const progressBar = document.getElementById('stackProgress'); - const progressText = document.getElementById('stackProgressText'); + const progressBar = document.getElementById('launchProgress'); + const progressText = document.getElementById('stepDescription'); while (Date.now() - startTime < maxWaitTime) { attempts++; diff --git a/templates/main/instances.html b/templates/main/instances.html index ec9feea..6f61e4c 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -226,6 +226,9 @@ + @@ -719,6 +722,76 @@ + + + {% endblock %} {% block extra_js %} diff --git a/templates/main/launch_progress.html b/templates/main/launch_progress.html index 7d88f01..d754e17 100644 --- a/templates/main/launch_progress.html +++ b/templates/main/launch_progress.html @@ -9,9 +9,9 @@ {% block content %} {{ header( - title="Launching Instance", - description="Setting up your new DocuPulse instance", - icon="fa-rocket" + title=is_update and "Updating Instance" or "Launching Instance", + description=is_update and "Updating your DocuPulse instance with the latest version" or "Setting up your new DocuPulse instance", + icon="fa-arrow-up" if is_update else "fa-rocket" ) }}
@@ -78,6 +78,12 @@ // Pass CSRF token to JavaScript window.csrfToken = '{{ csrf_token }}'; + + // Pass update parameters if this is an update operation + window.isUpdate = {{ 'true' if is_update else 'false' }}; + window.updateInstanceId = '{{ instance_id or "" }}'; + window.updateRepoId = '{{ repo_id or "" }}'; + window.updateBranch = '{{ branch or "" }}'; {% endblock %} \ No newline at end of file