Refactor upgrade status to use Livewire instead of API endpoint

- Remove /api/upgrade-status endpoint and route
- Add getUpgradeStatus() method to Upgrade Livewire component
- Update frontend to call Livewire method via $wire.getUpgradeStatus()
- Simpler approach: no separate API, auth handled by Livewire

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-12 21:37:32 +01:00
parent d111f448f5
commit deb9a7dab0
4 changed files with 92 additions and 153 deletions

View file

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use OpenApi\Attributes as OA;
@ -187,114 +186,4 @@ 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. Only available to root team members.',
path: '/upgrade-status',
operationId: 'upgrade-status',
security: [
['bearerAuth' => []],
],
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\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 403,
description: 'You are not allowed to view upgrade status.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to view upgrade status.'),
]
)),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function upgradeStatus(Request $request)
{
// Only root team members can view upgrade status
$user = auth()->user();
if (! $user || $user->currentTeam()->id !== 0) {
return response()->json(['message' => 'You are not allowed to view upgrade status.'], 403);
}
$server = Server::find(0);
if (! $server) {
return response()->json(['status' => 'none', 'debug' => 'no_server']);
}
$statusFile = '/data/coolify/source/.upgrade-status';
// Read status file from localhost via SSH
try {
$content = instant_remote_process(
["cat {$statusFile} 2>/dev/null || echo ''"],
$server,
false
);
$content = trim($content ?? '');
} catch (\Exception $e) {
return response()->json(['status' => 'none', 'debug' => 'ssh_exception', 'error' => $e->getMessage()]);
}
if (empty($content)) {
return response()->json(['status' => 'none', 'debug' => 'empty_content']);
}
$parts = explode('|', $content);
if (count($parts) < 3) {
return response()->json(['status' => 'none', 'debug' => 'invalid_parts', 'content' => $content]);
}
[$step, $message, $timestamp] = $parts;
// Check if status is stale (older than 10 minutes) - upgrades shouldn't take longer
try {
$statusTime = new \DateTime($timestamp);
$now = new \DateTime;
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
if ($diffMinutes > 10) {
return response()->json(['status' => 'none', 'debug' => 'stale', 'minutes' => $diffMinutes]);
}
} catch (\Exception $e) {
// If timestamp parsing fails, treat as stale for security
return response()->json(['status' => 'none', 'debug' => 'timestamp_error', 'timestamp' => $timestamp]);
}
// Determine status based on step
if ($step === 'error') {
return response()->json([
'status' => 'error',
'step' => 0,
'message' => $message,
]);
}
$stepInt = (int) $step;
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
return response()->json([
'status' => $status,
'step' => $stepInt,
'message' => $message,
]);
}
}

View file

@ -4,6 +4,7 @@
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Services\ChangelogService;
use Livewire\Component;
@ -78,4 +79,71 @@ public function upgrade()
return handleError($e, $this);
}
}
public function getUpgradeStatus(): array
{
// Only root team members can view upgrade status
if (auth()->user()?->currentTeam()->id !== 0) {
return ['status' => 'none'];
}
$server = Server::find(0);
if (! $server) {
return ['status' => 'none'];
}
$statusFile = '/data/coolify/source/.upgrade-status';
try {
$content = instant_remote_process(
["cat {$statusFile} 2>/dev/null || echo ''"],
$server,
false
);
$content = trim($content ?? '');
} catch (\Throwable $e) {
return ['status' => 'none'];
}
if (empty($content)) {
return ['status' => 'none'];
}
$parts = explode('|', $content);
if (count($parts) < 3) {
return ['status' => 'none'];
}
[$step, $message, $timestamp] = $parts;
// Check if status is stale (older than 10 minutes)
try {
$statusTime = new \DateTime($timestamp);
$now = new \DateTime;
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
if ($diffMinutes > 10) {
return ['status' => 'none'];
}
} catch (\Throwable $e) {
return ['status' => 'none'];
}
if ($step === 'error') {
return [
'status' => 'error',
'step' => 0,
'message' => $message,
];
}
$stepInt = (int) $step;
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
return [
'status' => $status,
'step' => $stepInt,
'message' => $message,
];
}
}

View file

@ -284,45 +284,32 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
this.currentStatus = 'Starting upgrade...';
this.serviceDown = false;
// Poll upgrade status API for real progress
this.checkUpgradeStatusInterval = setInterval(() => {
fetch('/api/upgrade-status', {
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
// Poll upgrade status via Livewire
this.checkUpgradeStatusInterval = setInterval(async () => {
try {
const data = await this.$wire.getUpgradeStatus();
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}`;
}
})
.then(response => {
if (response.ok) {
return response.json();
} catch (error) {
// Service is down - switch to health check mode
console.log('Livewire 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;
}
// Auth errors (401/403) or service down - switch to health check
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 => {
// 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);
}
}))

View file

@ -25,11 +25,6 @@
Route::get('/health', [OtherController::class, 'healthcheck']);
});
Route::middleware(['web', 'auth'])->group(function () {
Route::get('/upgrade-status', [OtherController::class, 'upgradeStatus']);
Route::get('/v1/upgrade-status', [OtherController::class, 'upgradeStatus']);
});
Route::post('/feedback', [OtherController::class, 'feedback']);
Route::group([