From 77032062a1ff6d8ef675d9849a26264df732b23e Mon Sep 17 00:00:00 2001 From: Kobe Date: Wed, 25 Jun 2025 15:07:35 +0200 Subject: [PATCH] better update? --- routes/launch_api.py | 273 +++++++++++++++++++++++++++-------- static/js/launch_progress.js | 241 ++++++++++++++++++++++++++----- 2 files changed, 415 insertions(+), 99 deletions(-) diff --git a/routes/launch_api.py b/routes/launch_api.py index 5fc3cb8..85ecb38 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -929,8 +929,8 @@ def deploy_stack(): def check_stack_status(): try: data = request.get_json() - if not data or 'stack_name' not in data: - return jsonify({'error': 'Missing stack_name field'}), 400 + if not data or ('stack_name' not in data and 'stack_id' not in data): + return jsonify({'error': 'Missing stack_name or stack_id field'}), 400 # Get Portainer settings portainer_settings = KeyValueSettings.get_value('portainer_settings') @@ -956,35 +956,54 @@ def check_stack_status(): endpoint_id = endpoints[0]['Id'] - # Get stack information - stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks" - stacks_response = requests.get( - stacks_url, - headers={ - 'X-API-Key': portainer_settings['api_key'], - 'Accept': 'application/json' - }, - params={'filters': json.dumps({'Name': data['stack_name']})}, - timeout=30 - ) + # Get stack information - support both stack_name and stack_id + if 'stack_id' in data: + # Get stack by ID + stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}" + stack_response = requests.get( + stack_url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + params={'endpointId': endpoint_id}, + timeout=30 + ) - if not stacks_response.ok: - return jsonify({'error': 'Failed to get stack information'}), 500 + if not stack_response.ok: + return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404 - stacks = stacks_response.json() - target_stack = None - - for stack in stacks: - if stack['Name'] == data['stack_name']: - target_stack = stack - break + target_stack = stack_response.json() + else: + # Get stack by name (existing logic) + stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks" + stacks_response = requests.get( + stacks_url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + params={'filters': json.dumps({'Name': data['stack_name']})}, + timeout=30 + ) - if not target_stack: - return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404 + if not stacks_response.ok: + return jsonify({'error': 'Failed to get stack information'}), 500 + + stacks = stacks_response.json() + target_stack = None + + for stack in stacks: + if stack['Name'] == data['stack_name']: + target_stack = stack + break + + if not target_stack: + return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404 # Get stack services to check their status services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services" - current_app.logger.info(f"Checking services for stack {data['stack_name']} at endpoint {endpoint_id}") + current_app.logger.info(f"Checking services for stack {target_stack['Name']} at endpoint {endpoint_id}") try: services_response = requests.get( @@ -993,7 +1012,7 @@ def check_stack_status(): 'X-API-Key': portainer_settings['api_key'], 'Accept': 'application/json' }, - params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})}, + params={'filters': json.dumps({'label': f'com.docker.stack.namespace={target_stack["Name"]}'})}, timeout=30 ) @@ -1001,46 +1020,40 @@ def check_stack_status(): if services_response.ok: services = services_response.json() - current_app.logger.info(f"Found {len(services)} services for stack {data['stack_name']}") - - # Check if all services are running - all_running = True service_statuses = [] for service in services: - replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0) - replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0) - - service_status = { + service_statuses.append({ 'name': service.get('Spec', {}).get('Name', 'Unknown'), - 'replicas_expected': replicas_running, - 'replicas_running': replicas_actual, - 'status': 'running' if replicas_actual >= replicas_running else 'not_running' - } - - service_statuses.append(service_status) - - if replicas_actual < replicas_running: - all_running = False - + 'replicas': service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0), + 'running_replicas': service.get('ServiceStatus', {}).get('RunningTasks', 0), + 'desired_replicas': service.get('ServiceStatus', {}).get('DesiredTasks', 0) + }) + # Determine overall stack status - if all_running and len(services) > 0: - status = 'active' - elif len(services) > 0: - status = 'partial' + if not service_statuses: + status = 'starting' # No services found yet else: - status = 'inactive' + all_running = all(s['running_replicas'] >= s['desired_replicas'] for s in service_statuses if s['desired_replicas'] > 0) + any_running = any(s['running_replicas'] > 0 for s in service_statuses) + + if all_running: + status = 'active' + elif any_running: + status = 'partial' + else: + status = 'inactive' else: # Services API failed, but stack exists - assume it's still starting up - current_app.logger.warning(f"Failed to get services for stack {data['stack_name']}: {services_response.status_code} - {services_response.text}") + current_app.logger.warning(f"Failed to get services for stack {target_stack['Name']}: {services_response.status_code} - {services_response.text}") # Provide more specific error context if services_response.status_code == 404: - current_app.logger.info(f"Services endpoint not found for stack {data['stack_name']} - stack may still be initializing") + current_app.logger.info(f"Services endpoint not found for stack {target_stack['Name']} - stack may still be initializing") elif services_response.status_code == 403: - current_app.logger.warning(f"Access denied to services for stack {data['stack_name']} - check Portainer permissions") + current_app.logger.warning(f"Access denied to services for stack {target_stack['Name']} - check Portainer permissions") elif services_response.status_code >= 500: - current_app.logger.warning(f"Portainer server error when getting services for stack {data['stack_name']}") + current_app.logger.warning(f"Portainer server error when getting services for stack {target_stack['Name']}") services = [] service_statuses = [] @@ -1048,7 +1061,7 @@ def check_stack_status(): except Exception as e: # Exception occurred while getting services, but stack exists - current_app.logger.warning(f"Exception getting services for stack {data['stack_name']}: {str(e)}") + current_app.logger.warning(f"Exception getting services for stack {target_stack['Name']}: {str(e)}") services = [] service_statuses = [] status = 'starting' # Stack exists but services not available yet @@ -1056,14 +1069,10 @@ def check_stack_status(): return jsonify({ 'success': True, 'data': { - 'stack_name': data['stack_name'], + 'name': target_stack['Name'], 'stack_id': target_stack['Id'], 'status': status, - 'services': service_statuses, - 'total_services': len(services), - 'running_services': len([s for s in service_statuses if s['status'] == 'running']), - 'stack_created_at': target_stack.get('CreatedAt', 'unknown'), - 'stack_updated_at': target_stack.get('UpdatedAt', 'unknown') + 'services': service_statuses } }) @@ -1854,4 +1863,148 @@ def copy_smtp_settings(): except Exception as e: current_app.logger.error(f"Error copying SMTP settings: {str(e)}") + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/update-stack', methods=['POST']) +@csrf.exempt +def update_stack(): + try: + data = request.get_json() + if not data or 'stack_id' not in data or 'StackFileContent' not in data: + return jsonify({'error': 'Missing required fields'}), 400 + + # Get Portainer settings + portainer_settings = KeyValueSettings.get_value('portainer_settings') + if not portainer_settings: + return jsonify({'error': 'Portainer settings not configured'}), 400 + + # Define timeout early to ensure it's available throughout the function + stack_timeout = current_app.config.get('STACK_DEPLOYMENT_TIMEOUT', 300) # Default to 5 minutes + + # Verify Portainer authentication + auth_response = requests.get( + f"{portainer_settings['url'].rstrip('/')}/api/status", + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + timeout=30 # 30 seconds timeout for status check + ) + + if not auth_response.ok: + current_app.logger.error(f"Portainer authentication failed: {auth_response.text}") + return jsonify({'error': 'Failed to authenticate with Portainer'}), 401 + + # Get Portainer endpoint ID (assuming it's the first endpoint) + endpoint_response = requests.get( + f"{portainer_settings['url'].rstrip('/')}/api/endpoints", + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + timeout=30 # 30 seconds timeout for endpoint check + ) + if not endpoint_response.ok: + error_text = endpoint_response.text + try: + error_json = endpoint_response.json() + error_text = error_json.get('message', error_text) + except: + pass + return jsonify({'error': f'Failed to get Portainer endpoints: {error_text}'}), 500 + + endpoints = endpoint_response.json() + if not endpoints: + return jsonify({'error': 'No Portainer endpoints found'}), 400 + + endpoint_id = endpoints[0]['Id'] + + # Log the request data + current_app.logger.info(f"Updating stack with ID: {data['stack_id']}") + current_app.logger.info(f"Using endpoint ID: {endpoint_id}") + current_app.logger.info(f"Using timeout: {stack_timeout} seconds") + + # First, verify the stack exists + stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}" + stack_response = requests.get( + stack_url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + params={'endpointId': endpoint_id}, + timeout=30 + ) + + if not stack_response.ok: + return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404 + + stack_info = stack_response.json() + current_app.logger.info(f"Found existing stack: {stack_info['Name']} (ID: {stack_info['Id']})") + + # Update the stack using Portainer's update API + update_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}/update" + current_app.logger.info(f"Making update request to: {update_url}") + + # Prepare the request body for stack update + request_body = { + 'StackFileContent': data['StackFileContent'], + 'Env': data.get('Env', []) + } + + # Add endpointId as a query parameter + params = {'endpointId': endpoint_id} + + # Use a configurable timeout for stack update initiation + update_response = requests.put( + update_url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + params=params, + json=request_body, + timeout=stack_timeout # Use configurable timeout + ) + + # Log the response details + current_app.logger.info(f"Update response status: {update_response.status_code}") + current_app.logger.info(f"Update response headers: {dict(update_response.headers)}") + + response_text = update_response.text + current_app.logger.info(f"Update response body: {response_text}") + + if not update_response.ok: + error_message = response_text + try: + error_json = update_response.json() + error_message = error_json.get('message', error_message) + except: + pass + return jsonify({'error': f'Failed to update stack: {error_message}'}), 500 + + # Stack update initiated successfully + current_app.logger.info(f"Stack update initiated: {stack_info['Name']} (ID: {stack_info['Id']})") + + return jsonify({ + 'success': True, + 'data': { + 'name': stack_info['Name'], + 'id': stack_info['Id'], + 'status': 'updating' + } + }) + + except requests.exceptions.Timeout: + current_app.logger.error(f"Request timed out after {stack_timeout} seconds while initiating stack update") + current_app.logger.error(f"Stack ID: {data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown'}") + current_app.logger.error(f"Portainer URL: {portainer_settings.get('url', 'unknown') if 'portainer_settings' in locals() else 'unknown'}") + return jsonify({ + 'error': f'Request timed out after {stack_timeout} seconds while initiating stack update. The operation may still be in progress.', + 'timeout_seconds': stack_timeout, + 'stack_id': data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown' + }), 504 + except Exception as e: + current_app.logger.error(f"Error updating stack: {str(e)}") return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 0896045..6a66c62 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -1395,7 +1395,7 @@ async function startUpdate(data) { // Step 3: Deploy Updated Stack await updateStep(3, 'Deploying Updated Stack', 'Deploying the updated application stack...'); - // Get the existing instance information to extract port + // Get the existing instance information to extract port and stack details const instanceResponse = await fetch(`/api/instances/${data.instanceId}`); if (!instanceResponse.ok) { throw new Error('Failed to get instance information'); @@ -1403,15 +1403,18 @@ async function startUpdate(data) { 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); + // Check if we have an existing stack ID + if (!instanceData.instance.portainer_stack_id) { + throw new Error('No existing stack found for this instance. Cannot perform update.'); + } - const stackResult = await deployStack(dockerComposeResult.content, newStackName, port); + // Update the existing stack instead of creating a new one + const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port); if (!stackResult.success) { - throw new Error(`Failed to deploy updated stack: ${stackResult.error}`); + throw new Error(`Failed to update stack: ${stackResult.error}`); } launchReport.steps.push({ - step: 'Stack Deployment', + step: 'Stack Update', status: 'success', details: stackResult }); @@ -1422,8 +1425,8 @@ async function startUpdate(data) { 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, + stack_id: instanceData.instance.portainer_stack_id, // Keep the same stack ID + stack_name: instanceData.instance.portainer_stack_name, // Keep the same stack name status: stackResult.data.status, repository: data.repository, branch: data.branch, @@ -1468,7 +1471,7 @@ async function startUpdate(data) { successDetails.className = 'mt-3'; // Calculate the volume names based on the stack name - const stackNameParts = newStackName.split('_'); + const stackNameParts = stackResult.data.name.split('_'); let volumeNames = []; if (stackNameParts.length >= 3) { const timestamp = stackNameParts.slice(2).join('_'); @@ -1482,7 +1485,7 @@ async function startUpdate(data) { successDetails.innerHTML = `
Update Completed Successfully!
-

Your instance has been updated with the latest version from the repository.

+

Your instance has been updated with the latest version from the repository. All existing data and volumes have been preserved.

Repository: ${data.repository}
@@ -1490,18 +1493,16 @@ async function startUpdate(data) { New Version: ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'}
- New Stack Name: ${newStackName}
+ Stack Name: ${stackResult.data.name}
Instance URL: ${instanceData.instance.main_url}
- ${volumeNames.length > 0 ? `
- New Volume Names: -
- ${volumeNames.map(name => `${name}`).join('
')} +
+ + Data Preservation: All existing data, volumes, and configurations have been preserved during this update.
- ` : ''}
`; successStep.querySelector('.step-content').appendChild(successDetails); @@ -3129,26 +3130,26 @@ async function deployStack(dockerComposeContent, stackName, port) { } // Function to poll stack status -async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { +async function pollStackStatus(stackIdentifier, maxWaitTime = 15 * 60 * 1000) { const startTime = Date.now(); const pollInterval = 5000; // 5 seconds let attempts = 0; let lastKnownStatus = 'unknown'; - // Validate stack name - if (!stackName || typeof stackName !== 'string') { - console.error('Invalid stack name provided to pollStackStatus:', stackName); + // Validate stack identifier (can be name or ID) + if (!stackIdentifier || typeof stackIdentifier !== 'string') { + console.error('Invalid stack identifier provided to pollStackStatus:', stackIdentifier); return { success: false, - error: `Invalid stack name: ${stackName}`, + error: `Invalid stack identifier: ${stackIdentifier}`, data: { - name: stackName, + name: stackIdentifier, status: 'error' } }; } - console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`); + console.log(`Starting to poll stack status for: ${stackIdentifier} (max wait: ${maxWaitTime / 1000}s)`); // Update progress indicator const progressBar = document.getElementById('launchProgress'); @@ -3156,7 +3157,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { while (Date.now() - startTime < maxWaitTime) { attempts++; - console.log(`Polling attempt ${attempts} for stack: ${stackName}`); + console.log(`Polling attempt ${attempts} for stack: ${stackIdentifier}`); // Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0% const elapsed = Date.now() - startTime; @@ -3168,9 +3169,12 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { } try { - const requestBody = { - stack_name: stackName - }; + // Determine if this is a stack ID (numeric) or stack name + const isStackId = /^\d+$/.test(stackIdentifier); + const requestBody = isStackId ? + { stack_id: stackIdentifier } : + { stack_name: stackIdentifier }; + console.log(`Sending stack status check request:`, requestBody); const response = await fetch('/api/admin/check-stack-status', { @@ -3188,7 +3192,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { console.log(`Stack status check result:`, result); if (result.data && result.data.status === 'active') { - console.log(`Stack ${stackName} is now active!`); + console.log(`Stack ${stackIdentifier} is now active!`); // Update progress to 100% if (progressBar && progressText) { progressBar.style.width = '100%'; @@ -3200,25 +3204,25 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { return { success: true, data: { - name: stackName, - id: result.data.stack_id, + name: result.data.name || stackIdentifier, + id: result.data.stack_id || stackIdentifier, status: 'active' } }; } else if (result.data && result.data.status === 'partial') { - console.log(`Stack ${stackName} is partially running, continuing to poll...`); + console.log(`Stack ${stackIdentifier} is partially running, continuing to poll...`); lastKnownStatus = 'partial'; if (progressText) { progressText.textContent = `Stack is partially running (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } else if (result.data && result.data.status === 'inactive') { - console.log(`Stack ${stackName} is inactive, continuing to poll...`); + console.log(`Stack ${stackIdentifier} is inactive, continuing to poll...`); lastKnownStatus = 'inactive'; if (progressText) { progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } else if (result.data && result.data.status === 'starting') { - console.log(`Stack ${stackName} exists and is starting up - continuing to next step`); + console.log(`Stack ${stackIdentifier} exists and is starting up - continuing to next step`); // Stack exists, we can continue - no need to wait for all services if (progressBar && progressText) { progressBar.style.width = '100%'; @@ -3230,20 +3234,20 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { return { success: true, data: { - name: stackName, - id: result.data.stack_id, + name: result.data.name || stackIdentifier, + id: result.data.stack_id || stackIdentifier, status: 'starting' } }; } else { - console.log(`Stack ${stackName} status unknown, continuing to poll...`); + console.log(`Stack ${stackIdentifier} status unknown, continuing to poll...`); lastKnownStatus = 'unknown'; if (progressText) { progressText.textContent = `Checking stack status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; } } } else if (response.status === 404) { - console.log(`Stack ${stackName} not found yet, continuing to poll...`); + console.log(`Stack ${stackIdentifier} not found yet, continuing to poll...`); lastKnownStatus = 'not_found'; if (progressText) { progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`; @@ -3280,11 +3284,11 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) { success: false, error: `Stack deployment timed out after ${Math.round(maxWaitTime / 1000)} seconds${statusMessage}. The stack may still be deploying in the background.`, data: { - name: stackName, + name: stackIdentifier, status: lastKnownStatus } }; -} +} // Helper function to generate unique stack names with timestamp function generateStackName(port) { @@ -3296,4 +3300,163 @@ function generateStackName(port) { now.getMinutes().toString().padStart(2, '0') + now.getSeconds().toString().padStart(2, '0'); return `docupulse_${port}_${timestamp}`; +} + +// Add new function to update existing stack +async function updateStack(dockerComposeContent, stackId, port) { + try { + console.log('Updating existing stack:', stackId); + console.log('Port:', port); + console.log('Modified docker-compose content length:', dockerComposeContent.length); + + const response = await fetch('/api/admin/update-stack', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + stack_id: stackId, + StackFileContent: dockerComposeContent, + Env: [ + { + name: 'PORT', + value: port.toString() + }, + { + name: 'ISMASTER', + value: 'false' + }, + { + name: 'APP_VERSION', + value: window.currentDeploymentVersion || 'unknown' + }, + { + name: 'GIT_COMMIT', + value: window.currentDeploymentCommit || 'unknown' + }, + { + name: 'GIT_BRANCH', + value: window.currentDeploymentBranch || 'unknown' + }, + { + name: 'DEPLOYED_AT', + value: new Date().toISOString() + } + ] + }) + }); + + console.log('Update response status:', response.status); + console.log('Update response ok:', response.ok); + console.log('Update response headers:', Object.fromEntries(response.headers.entries())); + + // Handle 504 Gateway Timeout as successful initiation + if (response.status === 504) { + console.log('Received 504 Gateway Timeout - stack update may still be in progress'); + + // Update progress to show that we're now polling + const progressBar = document.getElementById('launchProgress'); + const progressText = document.getElementById('stepDescription'); + if (progressBar && progressText) { + progressBar.style.width = '25%'; + progressBar.textContent = '25%'; + progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...'; + } + + // Start polling immediately since the stack update was initiated + console.log('Starting to poll for stack status after 504 timeout...'); + const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max + return pollResult; + } + + if (!response.ok) { + let errorMessage = 'Failed to update stack'; + console.log('Response not ok, status:', response.status); + + try { + const errorData = await response.json(); + errorMessage = errorData.error || errorMessage; + console.log('Parsed error data:', errorData); + } catch (parseError) { + console.log('Failed to parse JSON error, trying text:', parseError); + // If JSON parsing fails, try to get text content + try { + const errorText = await response.text(); + console.log('Error text content:', errorText); + if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) { + console.log('Received 504 Gateway Timeout - stack update may still be in progress'); + + // Update progress to show that we're now polling + const progressBar = document.getElementById('launchProgress'); + const progressText = document.getElementById('stepDescription'); + if (progressBar && progressText) { + progressBar.style.width = '25%'; + progressBar.textContent = '25%'; + progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...'; + } + + // Start polling immediately since the stack update was initiated + console.log('Starting to poll for stack status after 504 timeout...'); + const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max + return pollResult; + } else { + errorMessage = `HTTP ${response.status}: ${errorText}`; + } + } catch (textError) { + console.log('Failed to get error text:', textError); + errorMessage = `HTTP ${response.status}: Failed to parse response`; + } + } + console.log('Throwing error:', errorMessage); + throw new Error(errorMessage); + } + + const result = await response.json(); + console.log('Stack update initiated:', result); + + // If stack is being updated, poll for status + if (result.data.status === 'updating') { + console.log('Stack is being updated, polling for status...'); + const pollResult = await pollStackStatus(stackId, 10 * 60 * 1000); // 10 minutes max + return pollResult; + } + + // Return success result with response data + return { + success: true, + data: result.data + }; + + } catch (error) { + console.error('Error updating stack:', error); + + // Check if this is a 504 timeout error that should be handled as a success + if (error.message && ( + error.message.includes('504 Gateway Time-out') || + error.message.includes('504 Gateway Timeout') || + error.message.includes('timed out') + )) { + console.log('Detected 504 timeout in catch block - treating as successful initiation'); + + // Update progress to show that we're now polling + const progressBar = document.getElementById('launchProgress'); + const progressText = document.getElementById('stepDescription'); + if (progressBar && progressText) { + progressBar.style.width = '25%'; + progressBar.textContent = '25%'; + progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...'; + } + + // Start polling immediately since the stack update was initiated + console.log('Starting to poll for stack status after 504 timeout from catch block...'); + const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max + return pollResult; + } + + return { + success: false, + error: error.message + }; + } } \ No newline at end of file