Update start and better volume names

This commit is contained in:
2025-06-25 14:53:32 +02:00
parent 56d94a06ce
commit 0a2cddf122
6 changed files with 826 additions and 223 deletions

View File

@@ -774,6 +774,32 @@ def init_routes(main_bp):
return render_template('main/instance_detail.html', instance=instance) return render_template('main/instance_detail.html', instance=instance)
@main_bp.route('/api/instances/<int:instance_id>')
@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/<int:instance_id>/auth-status') @main_bp.route('/instances/<int:instance_id>/auth-status')
@login_required @login_required
@require_password_change @require_password_change
@@ -2139,6 +2165,12 @@ def init_routes(main_bp):
flash('This page is only available in master instances.', 'error') flash('This page is only available in master instances.', 'error')
return redirect(url_for('main.dashboard')) 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 # Get NGINX settings
nginx_settings = KeyValueSettings.get_value('nginx_settings') nginx_settings = KeyValueSettings.get_value('nginx_settings')
# Get Portainer settings # Get Portainer settings
@@ -2149,7 +2181,11 @@ def init_routes(main_bp):
return render_template('main/launch_progress.html', return render_template('main/launch_progress.html',
nginx_settings=nginx_settings, nginx_settings=nginx_settings,
portainer_settings=portainer_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']) @main_bp.route('/api/check-dns', methods=['POST'])
@login_required @login_required

View File

@@ -4,6 +4,7 @@ let editInstanceModal;
let addExistingInstanceModal; let addExistingInstanceModal;
let authModal; let authModal;
let launchStepsModal; let launchStepsModal;
let updateInstanceModal;
let currentStep = 1; let currentStep = 1;
// Update the total number of steps // Update the total number of steps
@@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal')); addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
authModal = new bootstrap.Modal(document.getElementById('authModal')); authModal = new bootstrap.Modal(document.getElementById('authModal'));
launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal')); launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal'));
updateInstanceModal = new bootstrap.Modal(document.getElementById('updateInstanceModal'));
// Initialize tooltips // Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
@@ -1775,3 +1777,167 @@ async function confirmDeleteInstance() {
}, 3000); }, 3000);
} }
} }
// 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 = '<option value="">Select a repository first</option>';
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 = '<option value="">Select a repository</option>' +
data.repositories.map(repo =>
`<option value="${repo.full_name}" ${repo.full_name === gitSettings.repo ? 'selected' : ''}>${repo.full_name}</option>`
).join('');
repoSelect.disabled = false;
// If we have a saved repository, load its branches
if (gitSettings.repo) {
loadUpdateBranches(gitSettings.repo);
}
} else {
repoSelect.innerHTML = '<option value="">No repositories found</option>';
repoSelect.disabled = true;
}
} catch (error) {
console.error('Error loading repositories for update:', error);
repoSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
repoSelect.disabled = true;
}
}
async function loadUpdateBranches(repoId) {
const branchSelect = document.getElementById('updateBranchSelect');
if (!repoId) {
branchSelect.innerHTML = '<option value="">Select a repository first</option>';
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 = '<option value="">Select a branch</option>' +
data.branches.map(branch =>
`<option value="${branch.name}" ${branch.name === 'master' ? 'selected' : ''}>${branch.name}</option>`
).join('');
branchSelect.disabled = false;
} else {
branchSelect.innerHTML = '<option value="">No branches found</option>';
branchSelect.disabled = true;
}
} catch (error) {
console.error('Error loading branches for update:', error);
branchSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
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);
});
}
});

View File

@@ -1,236 +1,280 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Get the launch data from sessionStorage // Check if this is an update operation
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); if (window.isUpdate && window.updateInstanceId && window.updateRepoId && window.updateBranch) {
if (!launchData) { // This is an update operation
showError('No launch data found. Please start over.'); const updateData = {
return; 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() { function initializeSteps() {
const stepsContainer = document.getElementById('stepsContainer'); const stepsContainer = document.getElementById('stepsContainer');
const isUpdate = window.isUpdate;
// Add Cloudflare connection check step if (isUpdate) {
const cloudflareStep = document.createElement('div'); // For updates, show fewer steps
cloudflareStep.className = 'step-item'; const steps = [
cloudflareStep.innerHTML = ` { icon: 'fab fa-docker', title: 'Checking Portainer Connection', description: 'Verifying connection to Portainer...' },
<div class="step-icon"><i class="fas fa-cloud"></i></div> { icon: 'fas fa-file-code', title: 'Downloading Docker Compose', description: 'Fetching docker-compose.yml from repository...' },
<div class="step-content"> { icon: 'fab fa-docker', title: 'Deploying Updated Stack', description: 'Deploying the updated application stack...' },
<h5>Checking Cloudflare Connection</h5> { icon: 'fas fa-save', title: 'Updating Instance Data', description: 'Updating instance information...' },
<p class="step-status">Verifying Cloudflare API connection...</p> { icon: 'fas fa-heartbeat', title: 'Health Check', description: 'Verifying updated instance health...' },
</div> { icon: 'fas fa-check-circle', title: 'Update Complete', description: 'Instance has been successfully updated!' }
`; ];
stepsContainer.appendChild(cloudflareStep);
// Add DNS record creation step steps.forEach((step, index) => {
const dnsCreateStep = document.createElement('div'); const stepElement = document.createElement('div');
dnsCreateStep.className = 'step-item'; stepElement.className = 'step-item';
dnsCreateStep.innerHTML = ` stepElement.innerHTML = `
<div class="step-icon"><i class="fas fa-plus-circle"></i></div> <div class="step-icon"><i class="${step.icon}"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Creating DNS Records</h5> <h5>${step.title}</h5>
<p class="step-status">Setting up domain DNS records in Cloudflare...</p> <p class="step-status">${step.description}</p>
</div> </div>
`; `;
stepsContainer.appendChild(dnsCreateStep); 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 = `
<div class="step-icon"><i class="fas fa-cloud"></i></div>
<div class="step-content">
<h5>Checking Cloudflare Connection</h5>
<p class="step-status">Verifying Cloudflare API connection...</p>
</div>
`;
stepsContainer.appendChild(cloudflareStep);
// Add DNS check step // Add DNS record creation step
const dnsStep = document.createElement('div'); const dnsCreateStep = document.createElement('div');
dnsStep.className = 'step-item'; dnsCreateStep.className = 'step-item';
dnsStep.innerHTML = ` dnsCreateStep.innerHTML = `
<div class="step-icon"><i class="fas fa-globe"></i></div> <div class="step-icon"><i class="fas fa-plus-circle"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Checking DNS Records</h5> <h5>Creating DNS Records</h5>
<p class="step-status">Verifying domain configurations...</p> <p class="step-status">Setting up domain DNS records in Cloudflare...</p>
</div> </div>
`; `;
stepsContainer.appendChild(dnsStep); stepsContainer.appendChild(dnsCreateStep);
// Add NGINX connection check step // Add DNS check step
const nginxStep = document.createElement('div'); const dnsStep = document.createElement('div');
nginxStep.className = 'step-item'; dnsStep.className = 'step-item';
nginxStep.innerHTML = ` dnsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-network-wired"></i></div> <div class="step-icon"><i class="fas fa-globe"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Checking NGINX Connection</h5> <h5>Checking DNS Records</h5>
<p class="step-status">Verifying connection to NGINX Proxy Manager...</p> <p class="step-status">Verifying domain configurations...</p>
</div> </div>
`; `;
stepsContainer.appendChild(nginxStep); stepsContainer.appendChild(dnsStep);
// Add SSL Certificate generation step // Add NGINX connection check step
const sslStep = document.createElement('div'); const nginxStep = document.createElement('div');
sslStep.className = 'step-item'; nginxStep.className = 'step-item';
sslStep.innerHTML = ` nginxStep.innerHTML = `
<div class="step-icon"><i class="fas fa-lock"></i></div> <div class="step-icon"><i class="fas fa-network-wired"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Generating SSL Certificate</h5> <h5>Checking NGINX Connection</h5>
<p class="step-status">Setting up secure HTTPS connection...</p> <p class="step-status">Verifying connection to NGINX Proxy Manager...</p>
</div> </div>
`; `;
stepsContainer.appendChild(sslStep); stepsContainer.appendChild(nginxStep);
// Add Proxy Host creation step // Add SSL Certificate generation step
const proxyStep = document.createElement('div'); const sslStep = document.createElement('div');
proxyStep.className = 'step-item'; sslStep.className = 'step-item';
proxyStep.innerHTML = ` sslStep.innerHTML = `
<div class="step-icon"><i class="fas fa-server"></i></div> <div class="step-icon"><i class="fas fa-lock"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Creating Proxy Host</h5> <h5>Generating SSL Certificate</h5>
<p class="step-status">Setting up NGINX proxy host configuration...</p> <p class="step-status">Setting up secure HTTPS connection...</p>
</div> </div>
`; `;
stepsContainer.appendChild(proxyStep); stepsContainer.appendChild(sslStep);
// Add Portainer connection check step // Add Proxy Host creation step
const portainerStep = document.createElement('div'); const proxyStep = document.createElement('div');
portainerStep.className = 'step-item'; proxyStep.className = 'step-item';
portainerStep.innerHTML = ` proxyStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div> <div class="step-icon"><i class="fas fa-server"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Checking Portainer Connection</h5> <h5>Creating Proxy Host</h5>
<p class="step-status">Verifying connection to Portainer...</p> <p class="step-status">Setting up NGINX proxy host configuration...</p>
</div> </div>
`; `;
stepsContainer.appendChild(portainerStep); stepsContainer.appendChild(proxyStep);
// Add Docker Compose download step // Add Portainer connection check step
const dockerComposeStep = document.createElement('div'); const portainerStep = document.createElement('div');
dockerComposeStep.className = 'step-item'; portainerStep.className = 'step-item';
dockerComposeStep.innerHTML = ` portainerStep.innerHTML = `
<div class="step-icon"><i class="fas fa-file-code"></i></div> <div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Downloading Docker Compose</h5> <h5>Checking Portainer Connection</h5>
<p class="step-status">Fetching docker-compose.yml from repository...</p> <p class="step-status">Verifying connection to Portainer...</p>
</div> </div>
`; `;
stepsContainer.appendChild(dockerComposeStep); stepsContainer.appendChild(portainerStep);
// Add Portainer stack deployment step // Add Docker Compose download step
const stackDeployStep = document.createElement('div'); const dockerComposeStep = document.createElement('div');
stackDeployStep.className = 'step-item'; dockerComposeStep.className = 'step-item';
stackDeployStep.innerHTML = ` dockerComposeStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div> <div class="step-icon"><i class="fas fa-file-code"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Deploying Stack</h5> <h5>Downloading Docker Compose</h5>
<p class="step-status">Launching your application stack...</p> <p class="step-status">Fetching docker-compose.yml from repository...</p>
</div> </div>
`; `;
stepsContainer.appendChild(stackDeployStep); stepsContainer.appendChild(dockerComposeStep);
// Add Save Instance Data step // Add Portainer stack deployment step
const saveDataStep = document.createElement('div'); const stackDeployStep = document.createElement('div');
saveDataStep.className = 'step-item'; stackDeployStep.className = 'step-item';
saveDataStep.innerHTML = ` stackDeployStep.innerHTML = `
<div class="step-icon"><i class="fas fa-save"></i></div> <div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Saving Instance Data</h5> <h5>Deploying Stack</h5>
<p class="step-status">Storing instance information...</p> <p class="step-status">Launching your application stack...</p>
</div> </div>
`; `;
stepsContainer.appendChild(saveDataStep); stepsContainer.appendChild(stackDeployStep);
// Add Health Check step // Add Save Instance Data step
const healthStep = document.createElement('div'); const saveDataStep = document.createElement('div');
healthStep.className = 'step-item'; saveDataStep.className = 'step-item';
healthStep.innerHTML = ` saveDataStep.innerHTML = `
<div class="step-icon"><i class="fas fa-heartbeat"></i></div> <div class="step-icon"><i class="fas fa-save"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Health Check</h5> <h5>Saving Instance Data</h5>
<p class="step-status">Verifying instance health...</p> <p class="step-status">Storing instance information...</p>
</div> </div>
`; `;
stepsContainer.appendChild(healthStep); stepsContainer.appendChild(saveDataStep);
// Add Authentication step // Add Health Check step
const authStep = document.createElement('div'); const healthStep = document.createElement('div');
authStep.className = 'step-item'; healthStep.className = 'step-item';
authStep.innerHTML = ` healthStep.innerHTML = `
<div class="step-icon"><i class="fas fa-key"></i></div> <div class="step-icon"><i class="fas fa-heartbeat"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Instance Authentication</h5> <h5>Health Check</h5>
<p class="step-status">Setting up instance authentication...</p> <p class="step-status">Verifying instance health...</p>
</div> </div>
`; `;
stepsContainer.appendChild(authStep); stepsContainer.appendChild(healthStep);
// Add Apply Company Information step // Add Authentication step
const companyStep = document.createElement('div'); const authStep = document.createElement('div');
companyStep.className = 'step-item'; authStep.className = 'step-item';
companyStep.innerHTML = ` authStep.innerHTML = `
<div class="step-icon"><i class="fas fa-building"></i></div> <div class="step-icon"><i class="fas fa-key"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Apply Company Information</h5> <h5>Instance Authentication</h5>
<p class="step-status">Configuring company details...</p> <p class="step-status">Setting up instance authentication...</p>
</div> </div>
`; `;
stepsContainer.appendChild(companyStep); stepsContainer.appendChild(authStep);
// Add Apply Colors step // Add Apply Company Information step
const colorsStep = document.createElement('div'); const companyStep = document.createElement('div');
colorsStep.className = 'step-item'; companyStep.className = 'step-item';
colorsStep.innerHTML = ` companyStep.innerHTML = `
<div class="step-icon"><i class="fas fa-palette"></i></div> <div class="step-icon"><i class="fas fa-building"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Apply Colors</h5> <h5>Apply Company Information</h5>
<p class="step-status">Configuring color scheme...</p> <p class="step-status">Configuring company details...</p>
</div> </div>
`; `;
stepsContainer.appendChild(colorsStep); stepsContainer.appendChild(companyStep);
// Add Update Admin Credentials step // Add Apply Colors step
const credentialsStep = document.createElement('div'); const colorsStep = document.createElement('div');
credentialsStep.className = 'step-item'; colorsStep.className = 'step-item';
credentialsStep.innerHTML = ` colorsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-user-shield"></i></div> <div class="step-icon"><i class="fas fa-palette"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Update Admin Credentials</h5> <h5>Apply Colors</h5>
<p class="step-status">Setting up admin account...</p> <p class="step-status">Configuring color scheme...</p>
</div> </div>
`; `;
stepsContainer.appendChild(credentialsStep); stepsContainer.appendChild(colorsStep);
// Add Copy SMTP Settings step // Add Update Admin Credentials step
const smtpStep = document.createElement('div'); const credentialsStep = document.createElement('div');
smtpStep.className = 'step-item'; credentialsStep.className = 'step-item';
smtpStep.innerHTML = ` credentialsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-envelope-open"></i></div> <div class="step-icon"><i class="fas fa-user-shield"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Copy SMTP Settings</h5> <h5>Update Admin Credentials</h5>
<p class="step-status">Configuring email settings...</p> <p class="step-status">Setting up admin account...</p>
</div> </div>
`; `;
stepsContainer.appendChild(smtpStep); stepsContainer.appendChild(credentialsStep);
// Add Send Completion Email step // Add Copy SMTP Settings step
const emailStep = document.createElement('div'); const smtpStep = document.createElement('div');
emailStep.className = 'step-item'; smtpStep.className = 'step-item';
emailStep.innerHTML = ` smtpStep.innerHTML = `
<div class="step-icon"><i class="fas fa-envelope"></i></div> <div class="step-icon"><i class="fas fa-envelope"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Send Completion Email</h5> <h5>Copy SMTP Settings</h5>
<p class="step-status">Sending notification to client...</p> <p class="step-status">Configuring email settings...</p>
</div> </div>
`; `;
stepsContainer.appendChild(emailStep); stepsContainer.appendChild(smtpStep);
// Add Download Launch Report step // Add Send Completion Email step
const reportStep = document.createElement('div'); const emailStep = document.createElement('div');
reportStep.className = 'step-item'; emailStep.className = 'step-item';
reportStep.innerHTML = ` emailStep.innerHTML = `
<div class="step-icon"><i class="fas fa-file-download"></i></div> <div class="step-icon"><i class="fas fa-paper-plane"></i></div>
<div class="step-content"> <div class="step-content">
<h5>Download Launch Report</h5> <h5>Send Completion Email</h5>
<p class="step-status">Preparing launch report...</p> <p class="step-status">Sending completion notification...</p>
</div> </div>
`; `;
stepsContainer.appendChild(reportStep); stepsContainer.appendChild(emailStep);
// Add Download Report step
const reportStep = document.createElement('div');
reportStep.className = 'step-item';
reportStep.innerHTML = `
<div class="step-icon"><i class="fas fa-download"></i></div>
<div class="step-content">
<h5>Download Launch Report</h5>
<p class="step-status">Preparing launch report...</p>
</div>
`;
stepsContainer.appendChild(reportStep);
}
} }
async function startLaunch(data) { async function startLaunch(data) {
@@ -467,7 +511,28 @@ async function startLaunch(data) {
downloadButton.className = 'btn btn-sm btn-primary mt-2'; downloadButton.className = 'btn btn-sm btn-primary mt-2';
downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml'; downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml';
downloadButton.onclick = () => { 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 url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@@ -554,6 +619,19 @@ async function startLaunch(data) {
// Add stack details // Add stack details
const stackDetails = document.createElement('div'); const stackDetails = document.createElement('div');
stackDetails.className = 'mt-3'; 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 = ` stackDetails.innerHTML = `
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@@ -583,9 +661,25 @@ async function startLaunch(data) {
</span> </span>
</td> </td>
</tr> </tr>
<tr>
<td>Volume Names</td>
<td>
<div class="small">
${volumeNames.length > 0 ? volumeNames.map(name =>
`<code class="text-primary">${name}</code>`
).join('<br>') : 'Using default volume names'}
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
${volumeNames.length > 0 ? `
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-2"></i>
<strong>Volume Naming Convention:</strong> Volumes have been named using the same timestamp as the stack for easy identification and management.
</div>
` : ''}
</div> </div>
</div> </div>
`; `;
@@ -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 = `
<div class="alert alert-success">
<h6><i class="fas fa-check-circle me-2"></i>Update Completed Successfully!</h6>
<p class="mb-2">Your instance has been updated with the latest version from the repository.</p>
<div class="row">
<div class="col-md-6">
<strong>Repository:</strong> ${data.repository}<br>
<strong>Branch:</strong> ${data.branch}<br>
<strong>New Version:</strong> ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'}
</div>
<div class="col-md-6">
<strong>New Stack Name:</strong> ${newStackName}<br>
<strong>Instance URL:</strong> <a href="${instanceData.instance.main_url}" target="_blank">${instanceData.instance.main_url}</a>
</div>
</div>
${volumeNames.length > 0 ? `
<div class="mt-3">
<strong>New Volume Names:</strong>
<div class="small mt-1">
${volumeNames.map(name => `<code class="text-primary">${name}</code>`).join('<br>')}
</div>
</div>
` : ''}
</div>
`;
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 = '<i class="fas fa-arrow-left me-2"></i>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) { async function checkDNSRecords(domains) {
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
const baseDelay = 10000; // 10 seconds base delay 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 // First, attempt to deploy the stack
const response = await fetch('/api/admin/deploy-stack', { const response = await fetch('/api/admin/deploy-stack', {
method: 'POST', method: 'POST',
@@ -2654,7 +2946,7 @@ async function deployStack(dockerComposeContent, stackName, port) {
}, },
body: JSON.stringify({ body: JSON.stringify({
name: stackName, name: stackName,
StackFileContent: dockerComposeContent, StackFileContent: modifiedDockerComposeContent,
Env: [ Env: [
{ {
name: 'PORT', 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'); console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
// Update progress to show that we're now polling // Update progress to show that we're now polling
const progressBar = document.getElementById('stackProgress'); const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stackProgressText'); const progressText = document.getElementById('stepDescription');
if (progressBar && progressText) { if (progressBar && progressText) {
progressBar.style.width = '25%'; progressBar.style.width = '25%';
progressBar.textContent = '25%'; progressBar.textContent = '25%';
@@ -2733,8 +3025,38 @@ async function deployStack(dockerComposeContent, stackName, port) {
} }
if (!response.ok) { if (!response.ok) {
const error = await response.json(); let errorMessage = 'Failed to deploy stack';
throw new Error(error.error || '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(); 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)`); console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`);
// Update progress indicator // Update progress indicator
const progressBar = document.getElementById('stackProgress'); const progressBar = document.getElementById('launchProgress');
const progressText = document.getElementById('stackProgressText'); const progressText = document.getElementById('stepDescription');
while (Date.now() - startTime < maxWaitTime) { while (Date.now() - startTime < maxWaitTime) {
attempts++; attempts++;

View File

@@ -226,6 +226,9 @@
<button class="btn btn-sm btn-outline-info" onclick="window.location.href='/instances/{{ instance.id }}/detail'"> <button class="btn btn-sm btn-outline-info" onclick="window.location.href='/instances/{{ instance.id }}/detail'">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
</button> </button>
<button class="btn btn-sm btn-outline-warning" onclick="showUpdateInstanceModal({{ instance.id }}, '{{ instance.portainer_stack_name }}', '{{ instance.main_url }}')" title="Update Instance">
<i class="fas fa-arrow-up"></i>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" onclick="showDeleteInstanceModal({{ instance.id }})"> <button class="btn btn-sm btn-outline-danger" type="button" onclick="showDeleteInstanceModal({{ instance.id }})">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@@ -719,6 +722,76 @@
</div> </div>
</div> </div>
<!-- Update Instance Modal -->
<div class="modal fade" id="updateInstanceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update Instance</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="updateInstanceForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" id="update_instance_id">
<input type="hidden" id="update_stack_name">
<input type="hidden" id="update_instance_url">
<div class="text-center mb-4">
<i class="fas fa-arrow-up fa-3x mb-3" style="color: var(--primary-color);"></i>
<h4>Update Instance</h4>
<p class="text-muted">Update your instance with the latest version from the repository.</p>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Repository</label>
<select class="form-select" id="updateRepoSelect">
<option value="">Loading repositories...</option>
</select>
<div class="form-text">Select the repository containing your application code</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Branch</label>
<select class="form-select" id="updateBranchSelect" disabled>
<option value="">Select a repository first</option>
</select>
<div class="form-text">Select the branch to deploy</div>
</div>
</div>
<div class="alert alert-info">
<h6><i class="fas fa-info-circle me-2"></i>Update Process:</h6>
<ul class="mb-0">
<li>Download the latest code from the selected repository and branch</li>
<li>Build a new Docker image with the updated code</li>
<li>Deploy the updated stack to replace the current instance</li>
<li>Preserve all existing data and configuration</li>
<li>Maintain the same port and domain configuration</li>
</ul>
</div>
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Important Notes:</h6>
<ul class="mb-0">
<li>The instance will be temporarily unavailable during the update process</li>
<li>All existing data, rooms, and conversations will be preserved</li>
<li>The update process may take several minutes to complete</li>
<li>You will be redirected to a progress page to monitor the update</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" onclick="startInstanceUpdate()">
<i class="fas fa-arrow-up me-1"></i>Start Update
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}

View File

@@ -9,9 +9,9 @@
{% block content %} {% block content %}
{{ header( {{ header(
title="Launching Instance", title=is_update and "Updating Instance" or "Launching Instance",
description="Setting up your new DocuPulse instance", description=is_update and "Updating your DocuPulse instance with the latest version" or "Setting up your new DocuPulse instance",
icon="fa-rocket" icon="fa-arrow-up" if is_update else "fa-rocket"
) }} ) }}
<div class="container-fluid"> <div class="container-fluid">
@@ -78,6 +78,12 @@
// Pass CSRF token to JavaScript // Pass CSRF token to JavaScript
window.csrfToken = '{{ csrf_token }}'; 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 "" }}';
</script> </script>
<script src="{{ url_for('static', filename='js/launch_progress.js') }}"></script> <script src="{{ url_for('static', filename='js/launch_progress.js') }}"></script>
{% endblock %} {% endblock %}