From b8cfc3f7c911661efae919c7b3cb9e7d8de8dcca Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:11:32 +0100 Subject: [PATCH] Add real-time upgrade progress tracking via status file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - upgrade.sh now writes status to /data/coolify/source/.upgrade-status - New /api/upgrade-status endpoint reads status file for real progress - Frontend polls status API instead of simulating progress - Falls back to health check when service goes down during restart 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Http/Controllers/Api/OtherController.php | 78 +++++++++++++++++ resources/views/livewire/upgrade.blade.php | 91 ++++++++++---------- routes/api.php | 2 + scripts/upgrade.sh | 24 ++++++ 4 files changed, 148 insertions(+), 47 deletions(-) diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 8f2ba25c8..64b2669e9 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -186,4 +186,82 @@ public function healthcheck(Request $request) { return 'OK'; } + + #[OA\Get( + summary: 'Upgrade Status', + description: 'Get the current upgrade status. Returns the step and message from the upgrade process.', + path: '/upgrade-status', + operationId: 'upgrade-status', + responses: [ + new OA\Response( + response: 200, + description: 'Returns upgrade status.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'status', type: 'string', example: 'in_progress'), + new OA\Property(property: 'step', type: 'integer', example: 3), + new OA\Property(property: 'message', type: 'string', example: 'Pulling Docker images'), + new OA\Property(property: 'timestamp', type: 'string', example: '2024-01-15T10:30:45+00:00'), + ] + )), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function upgradeStatus(Request $request) + { + $statusFile = '/data/coolify/source/.upgrade-status'; + + if (! file_exists($statusFile)) { + return response()->json(['status' => 'none']); + } + + $content = trim(file_get_contents($statusFile)); + if (empty($content)) { + return response()->json(['status' => 'none']); + } + + $parts = explode('|', $content); + if (count($parts) < 3) { + return response()->json(['status' => 'none']); + } + + [$step, $message, $timestamp] = $parts; + + // Check if status is stale (older than 30 minutes) + try { + $statusTime = new \DateTime($timestamp); + $now = new \DateTime; + $diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60; + + if ($diffMinutes > 30) { + return response()->json(['status' => 'none']); + } + } catch (\Exception $e) { + // If timestamp parsing fails, continue with the status + } + + // Determine status based on step + if ($step === 'error') { + return response()->json([ + 'status' => 'error', + 'step' => 0, + 'message' => $message, + 'timestamp' => $timestamp, + ]); + } + + $stepInt = (int) $step; + $status = $stepInt >= 6 ? 'complete' : 'in_progress'; + + return response()->json([ + 'status' => $status, + 'step' => $stepInt, + 'message' => $message, + 'timestamp' => $timestamp, + ]); + } } diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index eedadbb56..f379f24a9 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -161,7 +161,7 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel showProgress: false, currentStatus: '', checkHealthInterval: null, - checkIfIamDeadInterval: null, + checkUpgradeStatusInterval: null, elapsedInterval: null, healthCheckAttempts: 0, startTime: null, @@ -171,10 +171,12 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel successCountdown: 3, currentVersion: config.currentVersion || '', latestVersion: config.latestVersion || '', + serviceDown: false, confirmed() { this.showProgress = true; this.currentStep = 1; + this.currentStatus = 'Starting upgrade...'; this.startTimer(); this.$wire.$call('upgrade'); this.upgrade(); @@ -197,14 +199,14 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel return `${minutes}:${seconds.toString().padStart(2, '0')}`; }, - getStepMessage(step) { - const messages = { - 1: 'Preparing upgrade...', - 2: 'Pulling helper image...', - 3: 'Pulling Coolify image...', - 4: 'Restarting Coolify...' - }; - return messages[step] || 'Processing...'; + mapStepToUI(apiStep) { + // Map backend steps (1-6) to UI steps (1-4) + // Backend: 1=config, 2=env, 3=pull, 4=stop, 5=start, 6=complete + // UI: 1=prepare, 2=pull images, 3=pull coolify, 4=restart + if (apiStep <= 2) return 1; + if (apiStep === 3) return 2; + if (apiStep <= 5) return 3; + return 4; }, getReviveStatusMessage(elapsedMinutes, attempts) { @@ -249,6 +251,10 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel clearInterval(this.checkHealthInterval); this.checkHealthInterval = null; } + if (this.checkUpgradeStatusInterval) { + clearInterval(this.checkUpgradeStatusInterval); + this.checkUpgradeStatusInterval = null; + } if (this.elapsedInterval) { clearInterval(this.elapsedInterval); this.elapsedInterval = null; @@ -273,52 +279,43 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel }, upgrade() { - if (this.checkIfIamDeadInterval) return true; + if (this.checkUpgradeStatusInterval) return true; this.currentStep = 1; - this.currentStatus = this.getStepMessage(1); + this.currentStatus = 'Starting upgrade...'; + this.serviceDown = false; - // Simulate step progression (since we can't get real-time feedback from Docker pulls) - let stepTime = 0; - const stepInterval = setInterval(() => { - stepTime++; - // Progress through steps based on elapsed time - if (stepTime >= 3 && this.currentStep === 1) { - this.currentStep = 2; - this.currentStatus = this.getStepMessage(2); - } else if (stepTime >= 8 && this.currentStep === 2) { - this.currentStep = 3; - this.currentStatus = this.getStepMessage(3); - } - }, 1000); - - this.checkIfIamDeadInterval = setInterval(() => { - fetch('/api/health') + // Poll upgrade status API for real progress + this.checkUpgradeStatusInterval = setInterval(() => { + fetch('/api/upgrade-status') .then(response => { if (response.ok) { - // Still running, update status based on current step - this.currentStatus = this.getStepMessage(this.currentStep); - } else { - // Service is down, now waiting to revive - clearInterval(stepInterval); - this.currentStep = 4; - this.currentStatus = 'Coolify is restarting with the new version...'; - if (this.checkIfIamDeadInterval) { - clearInterval(this.checkIfIamDeadInterval); - this.checkIfIamDeadInterval = null; - } - this.revive(); + return response.json(); + } + throw new Error('Service unavailable'); + }) + .then(data => { + if (data.status === 'in_progress') { + this.currentStep = this.mapStepToUI(data.step); + this.currentStatus = data.message; + } else if (data.status === 'complete') { + this.showSuccess(); + } else if (data.status === 'error') { + this.currentStatus = `Error: ${data.message}`; } }) .catch(error => { - console.error('Health check failed:', error); - clearInterval(stepInterval); - this.currentStep = 4; - this.currentStatus = 'Coolify is restarting with the new version...'; - if (this.checkIfIamDeadInterval) { - clearInterval(this.checkIfIamDeadInterval); - this.checkIfIamDeadInterval = null; + // Service is down - switch to health check mode + console.log('Upgrade status API unavailable, switching to health check mode'); + if (!this.serviceDown) { + this.serviceDown = true; + this.currentStep = 4; + this.currentStatus = 'Coolify is restarting with the new version...'; + if (this.checkUpgradeStatusInterval) { + clearInterval(this.checkUpgradeStatusInterval); + this.checkUpgradeStatusInterval = null; + } + this.revive(); } - this.revive(); }); }, 2000); } diff --git a/routes/api.php b/routes/api.php index aaf7d794b..745a83534 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,10 +19,12 @@ use Illuminate\Support\Facades\Route; Route::get('/health', [OtherController::class, 'healthcheck']); +Route::get('/upgrade-status', [OtherController::class, 'upgradeStatus']); Route::group([ 'prefix' => 'v1', ], function () { Route::get('/health', [OtherController::class, 'healthcheck']); + Route::get('/upgrade-status', [OtherController::class, 'upgradeStatus']); }); Route::post('/feedback', [OtherController::class, 'feedback']); diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index b06cbc412..f6d0c9c8d 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -7,6 +7,7 @@ LATEST_HELPER_VERSION=${2:-latest} REGISTRY_URL=${3:-ghcr.io} SKIP_BACKUP=${4:-false} ENV_FILE="/data/coolify/source/.env" +STATUS_FILE="/data/coolify/source/.upgrade-status" DATE=$(date +%Y-%m-%d-%H-%M-%S) LOGFILE="/data/coolify/source/upgrade-${DATE}.log" @@ -24,6 +25,13 @@ log_section() { echo "============================================================" >>"$LOGFILE" } +# Helper function to write upgrade status for API polling +write_status() { + local step="$1" + local message="$2" + echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE" +} + echo "" echo "==========================================" echo " Coolify Upgrade - ${DATE}" @@ -40,6 +48,7 @@ echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE" echo "============================================================" >>"$LOGFILE" log_section "Step 1/6: Downloading configuration files" +write_status "1" "Downloading configuration files" echo "1/6 Downloading latest configuration files..." log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml" curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml @@ -63,6 +72,7 @@ if [ "$SKIP_BACKUP" != "true" ]; then fi log_section "Step 2/6: Updating environment configuration" +write_status "2" "Updating environment configuration" echo "" echo "2/6 Updating environment configuration..." log "Merging .env.production values into .env" @@ -115,6 +125,7 @@ if [ -f /root/.docker/config.json ]; then fi log_section "Step 3/6: Pulling Docker images" +write_status "3" "Pulling Docker images" echo "" echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." @@ -125,6 +136,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>" log "Successfully pulled Coolify image" else log "ERROR: Failed to pull Coolify image" + write_status "error" "Failed to pull Coolify image" echo " ERROR: Failed to pull Coolify image. Aborting upgrade." exit 1 fi @@ -135,6 +147,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELP log "Successfully pulled Coolify helper image" else log "ERROR: Failed to pull Coolify helper image" + write_status "error" "Failed to pull Coolify helper image" echo " ERROR: Failed to pull helper image. Aborting upgrade." exit 1 fi @@ -145,6 +158,7 @@ if docker pull postgres:15-alpine >>"$LOGFILE" 2>&1; then log "Successfully pulled PostgreSQL image" else log "ERROR: Failed to pull PostgreSQL image" + write_status "error" "Failed to pull PostgreSQL image" echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade." exit 1 fi @@ -155,6 +169,7 @@ if docker pull redis:7-alpine >>"$LOGFILE" 2>&1; then log "Successfully pulled Redis image" else log "ERROR: Failed to pull Redis image" + write_status "error" "Failed to pull Redis image" echo " ERROR: Failed to pull Redis image. Aborting upgrade." exit 1 fi @@ -165,6 +180,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>" log "Successfully pulled Coolify realtime image" else log "ERROR: Failed to pull Coolify realtime image" + write_status "error" "Failed to pull Coolify realtime image" echo " ERROR: Failed to pull realtime image. Aborting upgrade." exit 1 fi @@ -173,6 +189,7 @@ log "All images pulled successfully" echo " All images pulled successfully." log_section "Step 4/6: Stopping and restarting containers" +write_status "4" "Stopping containers" echo "" echo "4/6 Stopping containers and starting new ones..." echo " This step will restart all Coolify containers." @@ -185,6 +202,7 @@ log "Starting container restart sequence (detached)..." nohup bash -c " LOGFILE='$LOGFILE' + STATUS_FILE='$STATUS_FILE' DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT' REGISTRY_URL='$REGISTRY_URL' LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION' @@ -194,6 +212,10 @@ nohup bash -c " echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\" } + write_status() { + echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\" + } + # Stop and remove containers for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then @@ -213,6 +235,7 @@ nohup bash -c " echo '============================================================' >>\"\$LOGFILE\" log 'Step 5/6: Starting new containers' echo '============================================================' >>\"\$LOGFILE\" + write_status '5' 'Starting new containers' if [ -f /data/coolify/source/docker-compose.custom.yml ]; then log 'Using custom docker-compose.yml' @@ -230,6 +253,7 @@ nohup bash -c " echo '============================================================' >>\"\$LOGFILE\" log 'Step 6/6: Upgrade complete' echo '============================================================' >>\"\$LOGFILE\" + write_status '6' 'Upgrade complete' log 'Coolify upgrade completed successfully' log \"Version: \${LATEST_IMAGE}\" echo '' >>\"\$LOGFILE\"