From 0a2cddf1224e8f0c446d830ce6c1ecd764b5c392 Mon Sep 17 00:00:00 2001 From: Kobe Date: Wed, 25 Jun 2025 14:53:32 +0200 Subject: [PATCH] Update start and better volume names --- routes/__pycache__/main.cpython-313.pyc | Bin 119304 -> 120951 bytes routes/main.py | 38 +- static/js/instances.js | 168 +++++- static/js/launch_progress.js | 758 +++++++++++++++++------- templates/main/instances.html | 73 +++ templates/main/launch_progress.html | 12 +- 6 files changed, 826 insertions(+), 223 deletions(-) diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 23b05ba86fda0a55b77813d0ebd6e9fa0e0ffb83..91be6ce0a43c4b9385fc3c5904c86ef688e0b41f 100644 GIT binary patch delta 5832 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 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