From efd3a360d0f8947fdf7b29bc12738fd8bd8d6db6 Mon Sep 17 00:00:00 2001 From: aaryan359 Date: Tue, 12 Aug 2025 22:05:26 +0530 Subject: [PATCH 001/203] fix the ui for breadcrumbing --- .../resources/breadcrumbs.blade.php | 264 ++++++++++++++++-- 1 file changed, 235 insertions(+), 29 deletions(-) diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index 5f7029fd0..78f3f0ba1 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -5,45 +5,242 @@ ]) + + From 7af5d7683ab12f5aebbc7bb4a6c833362f1cb0fc Mon Sep 17 00:00:00 2001 From: aaryan359 Date: Sat, 23 Aug 2025 17:07:29 +0530 Subject: [PATCH 002/203] fix hover area and app level margin --- .../views/components/resources/breadcrumbs.blade.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index 78f3f0ba1..4d047c477 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -13,7 +13,7 @@ {{ data_get($resource, 'environment.project.name', 'Undefined Name') }} -
diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index b61887b6f..5c96e76ec 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -6,9 +6,10 @@ fullscreen: false, alwaysScroll: false, intervalId: null, + scrollDebounce: null, + searchTimeout: null, colorLogs: localStorage.getItem('coolify-color-logs') === 'true', searchQuery: '', - renderTrigger: 0, containerName: '{{ $container ?? "logs" }}', makeFullscreen() { this.fullscreen = !this.fullscreen; @@ -35,15 +36,22 @@ } }, handleScroll(event) { + // Skip if follow logs is disabled or this is a programmatic scroll if (!this.alwaysScroll || this.isScrolling) return; - const el = event.target; - // Check if user scrolled away from the bottom - const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; - if (distanceFromBottom > 50) { - this.alwaysScroll = false; - clearInterval(this.intervalId); - this.intervalId = null; - } + + // Debounce scroll handling to avoid false positives from DOM mutations + // when Livewire re-renders and adds new log lines + clearTimeout(this.scrollDebounce); + this.scrollDebounce = setTimeout(() => { + const el = event.target; + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; + // Use larger threshold (100px) to avoid accidental disables + if (distanceFromBottom > 100) { + this.alwaysScroll = false; + clearInterval(this.intervalId); + this.intervalId = null; + } + }, 150); }, toggleColorLogs() { this.colorLogs = !this.colorLogs; @@ -73,6 +81,12 @@ if (!this.searchQuery.trim()) return true; return line.toLowerCase().includes(this.searchQuery.toLowerCase()); }, + debouncedSearch(query) { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + this.searchQuery = query; + }, 300); + }, decodeHtml(text) { // Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS let decoded = text; @@ -160,12 +174,6 @@ this.$wire.getLogs(true); this.logsLoaded = true; } - // Re-render logs after Livewire updates - Livewire.hook('commit', ({ succeed }) => { - succeed(() => { - this.$nextTick(() => { this.renderTrigger++; }); - }); - }); } }"> @if ($collapsible) @@ -216,7 +224,7 @@ class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"> - @@ -32,9 +40,8 @@ class="flex items-center gap-2 px-4 py-2 rounded-lg shadow-lg transition-all dur
@@ -46,16 +53,15 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra @if ($deployment->status === 'in_progress') - + + @else - + @@ -68,7 +74,8 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra {{ $deployment->application_name }}

- {{ $deployment->application?->environment?->project?->name }} / {{ $deployment->application?->environment?->name }} + {{ $deployment->application?->environment?->project?->name }} / + {{ $deployment->application?->environment?->name }}

{{ $deployment->server_name }} @@ -78,11 +85,10 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra PR #{{ $deployment->pull_request_id }}

@endif -

+

{{ str_replace('_', ' ', $deployment->status) }}

@@ -92,4 +98,4 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra
@endif - + \ No newline at end of file diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index e5d1ce8e6..9f428bfde 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -256,7 +256,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
+ :class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
From 56102f6321403ad1014ce2043afa5f6a032c7091 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:25:22 +0100 Subject: [PATCH 056/203] Prevent multiple deploymentFinished event dispatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add flag to ensure event is only dispatched once, avoiding wasteful duplicate dispatches during the race condition window before Livewire removes wire:poll from the DOM. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Application/Deployment/Show.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 6d50fb3c7..44ab419c2 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -20,6 +20,8 @@ class Show extends Component public bool $is_debug_enabled = false; + private bool $deploymentFinishedDispatched = false; + public function getListeners() { return [ @@ -92,8 +94,9 @@ public function polling() $this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus(); $this->isKeepAliveOn(); - // Dispatch event when deployment finishes to stop auto-scroll - if (! $this->isKeepAliveOn) { + // Dispatch event when deployment finishes to stop auto-scroll (only once) + if (! $this->isKeepAliveOn && ! $this->deploymentFinishedDispatched) { + $this->deploymentFinishedDispatched = true; $this->dispatch('deploymentFinished'); } } From 206a9c03d24df3aeaef11609220cfa5dca73da24 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:25:35 +0100 Subject: [PATCH 057/203] Remove duplicate getArchDockerInstallCommand() method The method was defined twice with the first (outdated) definition using -Syyy and lacking proper flags. Keep the improved version that uses -Syu with --needed for idempotency and proper systemctl ordering. --- app/Actions/Server/InstallDocker.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 5caae6afc..c8713a22b 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -148,19 +148,6 @@ private function getSuseDockerInstallCommand(): string ')'; } - private function getArchDockerInstallCommand(): string - { - return 'pacman -Syyy --noconfirm && '. - 'pacman -S docker docker-compose --noconfirm && '. - 'systemctl start docker && '. - 'systemctl enable docker'; - } - - private function getGenericDockerInstallCommand(): string - { - return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}"; - } - private function getArchDockerInstallCommand(): string { // Use -Syu to perform full system upgrade before installing Docker @@ -171,4 +158,9 @@ private function getArchDockerInstallCommand(): string 'systemctl enable docker.service && '. 'systemctl start docker.service'; } + + private function getGenericDockerInstallCommand(): string + { + return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}"; + } } From 01308dede5a4e1bdfd11dc0c6e127916f51a77ae Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:39:55 +0100 Subject: [PATCH 058/203] Fix restart counter persistence and add crash loop example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move restart counter reset from Livewire to ApplicationDeploymentJob to prevent race conditions with GetContainersStatus - Remove artificial restart_type=manual tracking (never used in codebase) - Add Crash Loop Example in seeder for testing restart tracking UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Jobs/ApplicationDeploymentJob.php | 9 +++++++++ app/Livewire/Project/Application/Heading.php | 14 -------------- database/seeders/ApplicationSeeder.php | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6b13d2cb7..d6a4e129f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3980,6 +3980,15 @@ private function handleStatusTransition(ApplicationDeploymentStatus $status): vo */ private function handleSuccessfulDeployment(): void { + // Reset restart count after successful deployment + // This is done here (not in Livewire) to avoid race conditions + // with GetContainersStatus reading old container restart counts + $this->application->update([ + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + ]); + event(new ApplicationConfigurationChanged($this->application->team()->id)); if (! $this->only_this_server) { diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index fc63c7f4b..ec6b64492 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -106,13 +106,6 @@ public function deploy(bool $force_rebuild = false) return; } - // Reset restart count on successful deployment - $this->application->update([ - 'restart_count' => 0, - 'last_restart_at' => null, - 'last_restart_type' => null, - ]); - return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], @@ -157,13 +150,6 @@ public function restart() return; } - // Reset restart count on manual restart - $this->application->update([ - 'restart_count' => 0, - 'last_restart_at' => now(), - 'last_restart_type' => 'manual', - ]); - return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index f012c1534..ef5b4869d 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -75,6 +75,22 @@ public function run(): void 'dockerfile' => 'FROM nginx EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] +', + ]); + Application::create([ + 'name' => 'Crash Loop Example', + 'git_repository' => 'coollabsio/coolify', + 'git_branch' => 'v4.x', + 'git_commit_sha' => 'HEAD', + 'build_pack' => 'dockerfile', + 'ports_exposes' => '80', + 'environment_id' => 1, + 'destination_id' => 0, + 'destination_type' => StandaloneDocker::class, + 'source_id' => 0, + 'source_type' => GithubApp::class, + 'dockerfile' => 'FROM alpine +CMD ["sh", "-c", "echo Crashing in 5 seconds... && sleep 5 && exit 1"] ', ]); } From c6a89087c52412d64679ba9c4ed4d0dee94c2948 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:39:56 +0100 Subject: [PATCH 059/203] Refactor deployment indicator to use server-side route detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace client-side JavaScript URL checking with Laravel's routeIs() for determining when to reduce indicator opacity. This simplifies the code and uses route names as the source of truth. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/DeploymentsIndicator.php | 6 ++++++ .../views/livewire/deployments-indicator.blade.php | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/Livewire/DeploymentsIndicator.php b/app/Livewire/DeploymentsIndicator.php index 268aed152..5c945ac01 100644 --- a/app/Livewire/DeploymentsIndicator.php +++ b/app/Livewire/DeploymentsIndicator.php @@ -38,6 +38,12 @@ public function deploymentCount() return $this->deployments->count(); } + #[Computed] + public function shouldReduceOpacity(): bool + { + return request()->routeIs('project.application.deployment.*'); + } + public function toggleExpanded() { $this->expanded = ! $this->expanded; diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php index 9b71152af..2102004a1 100644 --- a/resources/views/livewire/deployments-indicator.blade.php +++ b/resources/views/livewire/deployments-indicator.blade.php @@ -1,17 +1,10 @@
@if ($this->deploymentCount > 0)
+ :class="{ 'opacity-100': expanded || !reduceOpacity, 'opacity-60 hover:opacity-100': reduceOpacity && !expanded }">
- @if ($resource?->build_pack === 'dockercompose') -
Please modify storage layout in your Docker Compose - file or reload the compose file to reread the storage layout.
- @else - @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0) -
No storage found.
- @endif + @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0) +
No storage found.
@endif - @php $hasVolumes = $this->volumeCount > 0; $hasFiles = $this->fileCount > 0; @@ -370,7 +364,6 @@ class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasDirectories ? 'dark

{{ Str::headline($resource->name) }}

- @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0)
No storage found.
@endif diff --git a/resources/views/livewire/project/shared/storages/all.blade.php b/resources/views/livewire/project/shared/storages/all.blade.php index d62362562..ea8e55e41 100644 --- a/resources/views/livewire/project/shared/storages/all.blade.php +++ b/resources/views/livewire/project/shared/storages/all.blade.php @@ -1,5 +1,11 @@
+ @if ($resource->type() === 'service' || data_get($resource, 'build_pack') === 'dockercompose') +
+ Volume mounts are read-only. If you would like to add or modify a volume, you must edit your Docker + Compose file and reload the compose file. +
+ @endif @foreach ($resource->persistentStorages as $storage) @if ($resource->type() === 'service')
@if ($isReadOnly) -
- This volume is mounted as read-only and cannot be modified from the UI. -
+ @if (!$storage->isServiceResource() && !$storage->isDockerComposeResource()) +
+ This volume is mounted as read-only and cannot be modified from the UI. +
+ @endif @if ($isFirst)
@if ( From eef0cc94cccaf1adea0e344d4f769760055ca4eb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:45:03 +0100 Subject: [PATCH 073/203] fix: Update documentation links in webhooks view to point to the correct API reference --- resources/views/livewire/project/shared/webhooks.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index ede1725b1..80fcfadc5 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -2,11 +2,11 @@

Webhooks

+ helper="For more details goto our docs." />
@if ($resource->type() === 'application') From 67b1db925460d21351babd9896b12de2b837879b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:14:32 +0100 Subject: [PATCH 074/203] feat: add Hetzner Cloud server linking for manually-added servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow manually-added servers to be linked to Hetzner Cloud instances by matching IP address. Once linked, servers gain power controls and status monitoring. Changes: - Add getServers() and findServerByIp() methods to HetznerService - Add Hetzner linking UI section to Server General page - Add unit tests for new HetznerService methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Livewire/Server/Show.php | 109 +++++++++++++ app/Services/HetznerService.php | 26 +++ .../views/livewire/server/show.blade.php | 58 +++++++ tests/Unit/HetznerServiceTest.php | 152 ++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 tests/Unit/HetznerServiceTest.php diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 4626a9135..7a4a1c480 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -5,9 +5,12 @@ use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Events\ServerReachabilityChanged; +use App\Models\CloudProviderToken; use App\Models\Server; +use App\Services\HetznerService; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Collection; use Livewire\Attributes\Computed; use Livewire\Attributes\Locked; use Livewire\Component; @@ -73,6 +76,17 @@ class Show extends Component public bool $isValidating = false; + // Hetzner linking properties + public Collection $availableHetznerTokens; + + public ?int $selectedHetznerTokenId = null; + + public ?array $matchedHetznerServer = null; + + public ?string $hetznerSearchError = null; + + public bool $hetznerNoMatchFound = false; + public function getListeners() { $teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id; @@ -150,6 +164,9 @@ public function mount(string $server_uuid) $this->hetznerServerStatus = $this->server->hetzner_server_status; $this->isValidating = $this->server->is_validating ?? false; + // Load Hetzner tokens for linking + $this->loadHetznerTokens(); + } catch (\Throwable $e) { return handleError($e, $this); } @@ -465,6 +482,98 @@ public function submit() } } + public function loadHetznerTokens(): void + { + $this->availableHetznerTokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function searchHetznerServer(): void + { + $this->hetznerSearchError = null; + $this->hetznerNoMatchFound = false; + $this->matchedHetznerServer = null; + + if (! $this->selectedHetznerTokenId) { + $this->hetznerSearchError = 'Please select a Hetzner token.'; + + return; + } + + try { + $this->authorize('update', $this->server); + + $token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId); + if (! $token) { + $this->hetznerSearchError = 'Invalid token selected.'; + + return; + } + + $hetznerService = new HetznerService($token->token); + $matched = $hetznerService->findServerByIp($this->server->ip); + + if ($matched) { + $this->matchedHetznerServer = $matched; + } else { + $this->hetznerNoMatchFound = true; + } + } catch (\Throwable $e) { + $this->hetznerSearchError = 'Failed to search Hetzner servers: '.$e->getMessage(); + } + } + + public function linkToHetzner() + { + if (! $this->matchedHetznerServer) { + $this->dispatch('error', 'No Hetzner server selected.'); + + return; + } + + try { + $this->authorize('update', $this->server); + + $token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId); + if (! $token) { + $this->dispatch('error', 'Invalid token selected.'); + + return; + } + + // Verify the server exists and is accessible with the token + $hetznerService = new HetznerService($token->token); + $serverData = $hetznerService->getServer($this->matchedHetznerServer['id']); + + if (empty($serverData)) { + $this->dispatch('error', 'Could not find Hetzner server with ID: '.$this->matchedHetznerServer['id']); + + return; + } + + // Update the server with Hetzner details + $this->server->update([ + 'cloud_provider_token_id' => $this->selectedHetznerTokenId, + 'hetzner_server_id' => $this->matchedHetznerServer['id'], + 'hetzner_server_status' => $serverData['status'] ?? null, + ]); + + $this->hetznerServerStatus = $serverData['status'] ?? null; + + // Clear the linking state + $this->matchedHetznerServer = null; + $this->selectedHetznerTokenId = null; + $this->hetznerNoMatchFound = false; + $this->hetznerSearchError = null; + + $this->dispatch('success', 'Server successfully linked to Hetzner Cloud!'); + $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.server.show'); diff --git a/app/Services/HetznerService.php b/app/Services/HetznerService.php index f7855090a..1de7eb2b1 100644 --- a/app/Services/HetznerService.php +++ b/app/Services/HetznerService.php @@ -161,4 +161,30 @@ public function deleteServer(int $serverId): void { $this->request('delete', "/servers/{$serverId}"); } + + public function getServers(): array + { + return $this->requestPaginated('get', '/servers', 'servers'); + } + + public function findServerByIp(string $ip): ?array + { + $servers = $this->getServers(); + + foreach ($servers as $server) { + // Check IPv4 + $ipv4 = data_get($server, 'public_net.ipv4.ip'); + if ($ipv4 === $ip) { + return $server; + } + + // Check IPv6 (Hetzner returns the full /64 block) + $ipv6 = data_get($server, 'public_net.ipv6.ip'); + if ($ipv6 && str_starts_with($ip, rtrim($ipv6, '/'))) { + return $server; + } + } + + return null; + } } diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index f9311bb83..a8344df05 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -320,6 +320,64 @@ class="w-full input opacity-50 cursor-not-allowed"
+ @if (!$server->hetzner_server_id && $availableHetznerTokens->isNotEmpty()) +
+

Link to Hetzner Cloud

+

+ Link this server to a Hetzner Cloud instance to enable power controls and status monitoring. +

+ +
+
+ + + @foreach ($availableHetznerTokens as $token) + + @endforeach + +
+ + Search by IP + Searching... + +
+ + @if ($hetznerSearchError) +
+

{{ $hetznerSearchError }}

+
+ @endif + + @if ($hetznerNoMatchFound) +
+

+ No Hetzner server found matching IP: {{ $server->ip }} +

+

+ Try a different token or verify the server IP is correct. +

+
+ @endif + + @if ($matchedHetznerServer) +
+

Match Found!

+
+
Name: {{ $matchedHetznerServer['name'] }}
+
ID: {{ $matchedHetznerServer['id'] }}
+
Status: {{ ucfirst($matchedHetznerServer['status']) }}
+
Type: {{ data_get($matchedHetznerServer, 'server_type.name', 'Unknown') }}
+
+ + Link This Server + +
+ @endif +
+ @endif @if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
diff --git a/tests/Unit/HetznerServiceTest.php b/tests/Unit/HetznerServiceTest.php new file mode 100644 index 000000000..7e76efdec --- /dev/null +++ b/tests/Unit/HetznerServiceTest.php @@ -0,0 +1,152 @@ + Http::response([ + 'servers' => [ + [ + 'id' => 12345, + 'name' => 'test-server-1', + 'status' => 'running', + 'public_net' => [ + 'ipv4' => ['ip' => '123.45.67.89'], + 'ipv6' => ['ip' => '2a01:4f8::/64'], + ], + ], + [ + 'id' => 67890, + 'name' => 'test-server-2', + 'status' => 'off', + 'public_net' => [ + 'ipv4' => ['ip' => '98.76.54.32'], + 'ipv6' => ['ip' => '2a01:4f9::/64'], + ], + ], + ], + 'meta' => ['pagination' => ['next_page' => null]], + ], 200), + ]); + + $service = new HetznerService('fake-token'); + $servers = $service->getServers(); + + expect($servers)->toBeArray() + ->and(count($servers))->toBe(2) + ->and($servers[0]['id'])->toBe(12345) + ->and($servers[1]['id'])->toBe(67890); +}); + +it('findServerByIp returns matching server by IPv4', function () { + Http::fake([ + 'api.hetzner.cloud/v1/servers*' => Http::response([ + 'servers' => [ + [ + 'id' => 12345, + 'name' => 'test-server', + 'status' => 'running', + 'public_net' => [ + 'ipv4' => ['ip' => '123.45.67.89'], + 'ipv6' => ['ip' => '2a01:4f8::/64'], + ], + ], + ], + 'meta' => ['pagination' => ['next_page' => null]], + ], 200), + ]); + + $service = new HetznerService('fake-token'); + $result = $service->findServerByIp('123.45.67.89'); + + expect($result)->not->toBeNull() + ->and($result['id'])->toBe(12345) + ->and($result['name'])->toBe('test-server'); +}); + +it('findServerByIp returns null when no match', function () { + Http::fake([ + 'api.hetzner.cloud/v1/servers*' => Http::response([ + 'servers' => [ + [ + 'id' => 12345, + 'name' => 'test-server', + 'status' => 'running', + 'public_net' => [ + 'ipv4' => ['ip' => '123.45.67.89'], + 'ipv6' => ['ip' => '2a01:4f8::/64'], + ], + ], + ], + 'meta' => ['pagination' => ['next_page' => null]], + ], 200), + ]); + + $service = new HetznerService('fake-token'); + $result = $service->findServerByIp('1.2.3.4'); + + expect($result)->toBeNull(); +}); + +it('findServerByIp returns null when server list is empty', function () { + Http::fake([ + 'api.hetzner.cloud/v1/servers*' => Http::response([ + 'servers' => [], + 'meta' => ['pagination' => ['next_page' => null]], + ], 200), + ]); + + $service = new HetznerService('fake-token'); + $result = $service->findServerByIp('123.45.67.89'); + + expect($result)->toBeNull(); +}); + +it('findServerByIp matches correct server among multiple', function () { + Http::fake([ + 'api.hetzner.cloud/v1/servers*' => Http::response([ + 'servers' => [ + [ + 'id' => 11111, + 'name' => 'server-a', + 'status' => 'running', + 'public_net' => [ + 'ipv4' => ['ip' => '10.0.0.1'], + 'ipv6' => ['ip' => '2a01:4f8::/64'], + ], + ], + [ + 'id' => 22222, + 'name' => 'server-b', + 'status' => 'running', + 'public_net' => [ + 'ipv4' => ['ip' => '10.0.0.2'], + 'ipv6' => ['ip' => '2a01:4f9::/64'], + ], + ], + [ + 'id' => 33333, + 'name' => 'server-c', + 'status' => 'off', + 'public_net' => [ + 'ipv4' => ['ip' => '10.0.0.3'], + 'ipv6' => ['ip' => '2a01:4fa::/64'], + ], + ], + ], + 'meta' => ['pagination' => ['next_page' => null]], + ], 200), + ]); + + $service = new HetznerService('fake-token'); + $result = $service->findServerByIp('10.0.0.2'); + + expect($result)->not->toBeNull() + ->and($result['id'])->toBe(22222) + ->and($result['name'])->toBe('server-b'); +}); From b354ab9a3050457683af06d92fc92e809942a15a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:29:14 +0100 Subject: [PATCH 075/203] Fix execution time tooltip maximum value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update tooltip text to correctly show that the maximum execution time is 36000 seconds (10 hours) instead of 3600 seconds. The validation already allowed up to 36000 seconds, but the UI was displaying incorrect information. 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 --- .../views/livewire/project/shared/scheduled-task/add.blade.php | 2 +- .../views/livewire/project/shared/scheduled-task/show.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/scheduled-task/add.blade.php b/resources/views/livewire/project/shared/scheduled-task/add.blade.php index 6fa04c28b..aac61e3d4 100644 --- a/resources/views/livewire/project/shared/scheduled-task/add.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/add.blade.php @@ -5,7 +5,7 @@ helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency" label="Frequency" /> @if ($type === 'application') @if ($containerNames->count() > 1) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index fa2ce0ad9..f312c0bf3 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -36,7 +36,7 @@ + helper="Maximum execution time in seconds (60-36000)." label="Timeout (seconds)" required /> @if ($type === 'application') Date: Fri, 12 Dec 2025 10:31:25 +0530 Subject: [PATCH 076/203] [service] fixed postiz showing no available server due to outdated node version --- templates/compose/postiz.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/postiz.yaml b/templates/compose/postiz.yaml index 33aeebbc6..72fcb3c0b 100644 --- a/templates/compose/postiz.yaml +++ b/templates/compose/postiz.yaml @@ -7,7 +7,7 @@ services: postiz: - image: ghcr.io/gitroomhq/postiz-app:v1.60.1 + image: ghcr.io/gitroomhq/postiz-app:v2.10.1 environment: - SERVICE_URL_POSTIZ_5000 - MAIN_URL=${SERVICE_URL_POSTIZ} From 546256b22cca128021280a25242882e9f352993e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:12:19 +0100 Subject: [PATCH 077/203] Fix: Allow test emails to be sent to any email address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test emails should work with any recipient email address for verification purposes, not just team members. Added an isTestNotification flag to both Test notification classes and modified EmailChannel to skip team membership validation for test notifications. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- app/Notifications/Channels/EmailChannel.php | 35 +++++++++++-------- app/Notifications/Test.php | 2 ++ .../TransactionalEmails/Test.php | 2 ++ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 234bc37ad..abd115550 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -43,21 +43,26 @@ public function send(SendsEmail $notifiable, Notification $notification): void throw new Exception('No email recipients found'); } - foreach ($recipients as $recipient) { - // Check if the recipient is part of the team - if (! $members->contains('email', $recipient)) { - $emailSettings = $notifiable->emailNotificationSettings; - data_set($emailSettings, 'smtp_password', '********'); - data_set($emailSettings, 'resend_api_key', '********'); - send_internal_notification(sprintf( - "Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s", - $recipient, - $team, - get_class($notification), - get_class($notifiable), - json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - )); - throw new Exception('Recipient is not part of the team'); + // Skip team membership validation for test notifications + $isTestNotification = data_get($notification, 'isTestNotification', false); + + if (! $isTestNotification) { + foreach ($recipients as $recipient) { + // Check if the recipient is part of the team + if (! $members->contains('email', $recipient)) { + $emailSettings = $notifiable->emailNotificationSettings; + data_set($emailSettings, 'smtp_password', '********'); + data_set($emailSettings, 'resend_api_key', '********'); + send_internal_notification(sprintf( + "Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s", + $recipient, + $team, + get_class($notification), + get_class($notifiable), + json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + )); + throw new Exception('Recipient is not part of the team'); + } } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 60bc8a0ee..bbed22777 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -23,6 +23,8 @@ class Test extends Notification implements ShouldQueue public $tries = 5; + public bool $isTestNotification = true; + public function __construct(public ?string $emails = null, public ?string $channel = null, public ?bool $ping = false) { $this->onQueue('high'); diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index 3add70db2..2f7d70bbf 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -8,6 +8,8 @@ class Test extends CustomEmailNotification { + public bool $isTestNotification = true; + public function __construct(public string $emails, public bool $isTransactionalEmail = true) { $this->onQueue('high'); From de59096c9d328abe9048a27f2c99f9bebc4ca643 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:21:54 +0100 Subject: [PATCH 078/203] Bump version to v455 (#7601) Co-authored-by: Claude Haiku 4.5 --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index 15ec73625..d9734c48e 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.454', + 'version' => '4.0.0-beta.455', 'helper_version' => '1.0.12', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 1441c7c5e..94c23ede4 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.454" + "version": "4.0.0-beta.455" }, "nightly": { - "version": "4.0.0-beta.455" + "version": "4.0.0-beta.456" }, "helper": { "version": "1.0.12" diff --git a/versions.json b/versions.json index 1441c7c5e..94c23ede4 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.454" + "version": "4.0.0-beta.455" }, "nightly": { - "version": "4.0.0-beta.455" + "version": "4.0.0-beta.456" }, "helper": { "version": "1.0.12" From 0c46da0a23708205917e5eb830459882a60bdc0e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:28:53 +0100 Subject: [PATCH 079/203] Fix Docker container race condition during upgrades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop and remove existing Coolify containers before starting new ones to prevent conflicts when project name changes from 'source' to 'coolify'. This resolves volume and container name conflicts during upgrades from older installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- other/nightly/upgrade.sh | 11 +++++++++++ scripts/upgrade.sh | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index bfcd11095..667cf4162 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -64,6 +64,17 @@ if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi +# Stop and remove existing Coolify containers to prevent conflicts +# This handles both old installations (project "source") and new ones (project "coolify") +echo "Stopping existing Coolify containers..." >>"$LOGFILE" +for container in coolify coolify-db coolify-redis coolify-realtime; do + if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then + docker stop "$container" >>"$LOGFILE" 2>&1 || true + docker rm "$container" >>"$LOGFILE" 2>&1 || true + echo " - Removed container: $container" >>"$LOGFILE" + fi +done + if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo "docker-compose.custom.yml detected." >>"$LOGFILE" docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index f091d2fdb..36746679b 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -64,6 +64,17 @@ if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi +# Stop and remove existing Coolify containers to prevent conflicts +# This handles both old installations (project "source") and new ones (project "coolify") +echo "Stopping existing Coolify containers..." >>"$LOGFILE" +for container in coolify coolify-db coolify-redis coolify-realtime; do + if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then + docker stop "$container" >>"$LOGFILE" 2>&1 || true + docker rm "$container" >>"$LOGFILE" 2>&1 || true + echo " - Removed container: $container" >>"$LOGFILE" + fi +done + if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo "docker-compose.custom.yml detected." >>"$LOGFILE" docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 From 0e47de81d195b77865776f7e76b54d461391a24a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:35:00 +0100 Subject: [PATCH 080/203] Fix: Prevent double deployments when multiple GitHub Apps access same repository (#2315) Filter webhook-triggered deployments by source_id to ensure only applications associated with the GitHub App that sent the webhook are deployed, preventing duplicate deployments when the same repository is configured in multiple teams. --- app/Http/Controllers/Webhook/Github.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index b2c211fa8..c8402cbf4 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -309,7 +309,9 @@ public function normal(Request $request) if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); } - $applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false); + $applications = Application::where('repository_project_id', $id) + ->where('source_id', $github_app->id) + ->whereRelation('source', 'is_public', false); if ($x_github_event === 'push') { $applications = $applications->where('git_branch', $branch)->get(); if ($applications->isEmpty()) { From b0d50669b1b8929b3c82ee4103fb3d1f2a1b0bf1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:12:02 +0100 Subject: [PATCH 081/203] fix: skip password confirmation for OAuth users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OAuth users don't have passwords set, so they should not be prompted for password confirmation when performing destructive actions. This fix: - Detects OAuth users via the hasPassword() method - Skips password confirmation in modal for OAuth users - Keeps text name confirmation as the final step - Centralizes logic in helper functions for maintainability - Changes button text to "Confirm" when password step is skipped Fixes #4457 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- app/Livewire/NavbarDeleteTeam.php | 10 +--- app/Livewire/Project/Database/BackupEdit.php | 11 +--- .../Project/Database/BackupExecutions.php | 10 +--- app/Livewire/Project/Service/Database.php | 11 +--- app/Livewire/Project/Service/FileStorage.php | 11 +--- .../Service/ServiceApplicationView.php | 11 +--- app/Livewire/Project/Shared/Danger.php | 11 +--- app/Livewire/Project/Shared/Destination.php | 11 +--- app/Livewire/Project/Shared/Storages/Show.php | 11 +--- app/Livewire/Server/Delete.php | 11 +--- .../Server/Security/TerminalAccess.php | 13 +---- app/Livewire/Settings/Advanced.php | 6 +- app/Livewire/Team/AdminView.php | 11 +--- app/Models/User.php | 9 +++ bootstrap/helpers/shared.php | 55 +++++++++++++++++++ .../components/modal-confirmation.blade.php | 27 ++++++--- 16 files changed, 109 insertions(+), 120 deletions(-) diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index e97cceb0d..9508c2adc 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -2,10 +2,8 @@ namespace App\Livewire; -use App\Models\InstanceSettings; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class NavbarDeleteTeam extends Component @@ -19,12 +17,8 @@ public function mount() public function delete($password) { - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } $currentTeam = currentTeam(); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 18ad93016..d70c52411 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -2,12 +2,9 @@ namespace App\Livewire\Project\Database; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; @@ -154,12 +151,8 @@ public function delete($password) { $this->authorize('manageBackups', $this->backup->database); - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } try { diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 0b6d8338b..44f903fcc 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,11 +2,9 @@ namespace App\Livewire\Project\Database; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class BackupExecutions extends Component @@ -69,12 +67,8 @@ public function cleanupDeleted() public function deleteBackup($executionId, $password) { - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } $execution = $this->backup->executions()->where('id', $executionId)->first(); diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index 4bcf866d3..1e183c6bc 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -4,12 +4,9 @@ use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; -use App\Models\InstanceSettings; use App\Models\ServiceDatabase; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class Database extends Component @@ -96,12 +93,8 @@ public function delete($password) try { $this->authorize('delete', $this->database); - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } $this->database->delete(); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 54ef82872..079115bb6 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Service; use App\Models\Application; -use App\Models\InstanceSettings; use App\Models\LocalFileVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -16,8 +15,6 @@ use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Validate; use Livewire\Component; @@ -141,12 +138,8 @@ public function delete($password) { $this->authorize('update', $this->resource); - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } try { diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 68544f1ab..4302c05fb 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,12 +2,9 @@ namespace App\Livewire\Project\Service; -use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; @@ -128,12 +125,8 @@ public function delete($password) try { $this->authorize('delete', $this->application); - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } $this->application->delete(); diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 0ed1347f8..8bf3c7438 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -3,13 +3,10 @@ namespace App\Livewire\Project\Shared; use App\Jobs\DeleteResourceJob; -use App\Models\InstanceSettings; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -93,12 +90,8 @@ public function mount() public function delete($password) { - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } if (! $this->resource) { diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 28e3f23e7..ffd18b35c 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -5,12 +5,9 @@ use App\Actions\Application\StopApplicationOneServer; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -140,12 +137,8 @@ public function addServer(int $network_id, int $server_id) public function removeServer(int $network_id, int $server_id, $password) { try { - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index c8dc68d66..2091eca14 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -2,11 +2,8 @@ namespace App\Livewire\Project\Shared\Storages; -use App\Models\InstanceSettings; use App\Models\LocalPersistentVolume; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class Show extends Component @@ -84,12 +81,8 @@ public function delete($password) { $this->authorize('update', $this->resource); - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } $this->storage->delete(); diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index 8c2c54c99..27a6e7aca 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -3,11 +3,8 @@ namespace App\Livewire\Server; use App\Actions\Server\DeleteServer; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class Delete extends Component @@ -29,12 +26,8 @@ public function mount(string $server_uuid) public function delete($password) { - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } try { $this->authorize('delete', $this->server); diff --git a/app/Livewire/Server/Security/TerminalAccess.php b/app/Livewire/Server/Security/TerminalAccess.php index 284eea7dd..310edcfe4 100644 --- a/app/Livewire/Server/Security/TerminalAccess.php +++ b/app/Livewire/Server/Security/TerminalAccess.php @@ -2,11 +2,8 @@ namespace App\Livewire\Server\Security; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Validate; use Livewire\Component; @@ -44,13 +41,9 @@ public function toggleTerminal($password) throw new \Exception('Only team administrators and owners can modify terminal access.'); } - // Verify password unless two-step confirmation is disabled - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + // Verify password + if (! verifyPasswordConfirmation($password, $this)) { + return; } // Toggle the terminal setting diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index be38ae1d8..b011d2dc1 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -5,8 +5,6 @@ use App\Models\InstanceSettings; use App\Models\Server; use App\Rules\ValidIpOrCidr; -use Auth; -use Hash; use Livewire\Attributes\Validate; use Livewire\Component; @@ -157,9 +155,7 @@ public function instantSave() public function toggleTwoStepConfirmation($password): bool { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - + if (! verifyPasswordConfirmation($password, $this)) { return false; } diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 6d6915ae2..c8d44d42b 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -2,10 +2,7 @@ namespace App\Livewire\Team; -use App\Models\InstanceSettings; use App\Models\User; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class AdminView extends Component @@ -58,12 +55,8 @@ public function delete($id, $password) return redirect()->route('dashboard'); } - if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return; - } + if (! verifyPasswordConfirmation($password, $this)) { + return; } if (! auth()->user()->isInstanceAdmin()) { diff --git a/app/Models/User.php b/app/Models/User.php index f04b6fa77..b790efcf1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -443,4 +443,13 @@ public function hasEmailChangeRequest(): bool && $this->email_change_code_expires_at && Carbon::now()->lessThan($this->email_change_code_expires_at); } + + /** + * Check if the user has a password set. + * OAuth users are created without passwords. + */ + public function hasPassword(): bool + { + return ! empty($this->password); + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1066f1a63..3d9e9e729 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -33,6 +33,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\RateLimiter; @@ -3308,3 +3309,57 @@ function formatContainerStatus(string $status): string return str($status)->headline()->value(); } } + +/** + * Check if password confirmation should be skipped. + * Returns true if: + * - Two-step confirmation is globally disabled + * - User has no password (OAuth users) + * + * Used by modal-confirmation.blade.php to determine if password step should be shown. + * + * @return bool True if password confirmation should be skipped + */ +function shouldSkipPasswordConfirmation(): bool +{ + // Skip if two-step confirmation is globally disabled + if (data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + return true; + } + + // Skip if user has no password (OAuth users) + if (! Auth::user()?->hasPassword()) { + return true; + } + + return false; +} + +/** + * Verify password for two-step confirmation. + * Skips verification if: + * - Two-step confirmation is globally disabled + * - User has no password (OAuth users) + * + * @param mixed $password The password to verify (may be array if skipped by frontend) + * @param \Livewire\Component|null $component Optional Livewire component to add errors to + * @return bool True if verification passed (or skipped), false if password is incorrect + */ +function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $component = null): bool +{ + // Skip if password confirmation should be skipped + if (shouldSkipPasswordConfirmation()) { + return true; + } + + // Verify the password + if (! Hash::check($password, Auth::user()->password)) { + if ($component) { + $component->addError('password', 'The provided password is incorrect.'); + } + + return false; + } + + return true; +} diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index edff3b6bf..73939092e 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -29,17 +29,23 @@ @php use App\Models\InstanceSettings; + // Global setting to disable ALL two-step confirmation (text + password) $disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation'); + // Skip ONLY password confirmation for OAuth users (they have no password) + $skipPasswordConfirmation = shouldSkipPasswordConfirmation(); if ($temporaryDisableTwoStepConfirmation) { $disableTwoStepConfirmation = false; + $skipPasswordConfirmation = false; } + // When password step is skipped, Step 2 becomes final - change button text from "Continue" to "Confirm" + $effectiveStep2ButtonText = ($skipPasswordConfirmation && $step2ButtonText === 'Continue') ? 'Confirm' : $step2ButtonText; @endphp
- @if (!$disableTwoStepConfirmation) + @if (!$skipPasswordConfirmation)
Please enter your password to confirm this destructive action. From c45cbc04c8c9627c299d5a0e8463aaf47a5f5121 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:54:24 +0100 Subject: [PATCH 082/203] Pull images before stopping containers during upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensures images are available before taking down the system. If pull fails (rate limits, network issues, expired tokens), upgrade aborts safely without leaving Coolify in a broken state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- other/nightly/upgrade.sh | 11 +++++++++++ scripts/upgrade.sh | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 667cf4162..aa0c82865 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -64,6 +64,17 @@ if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi +# Pull all required images before stopping containers +# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.) +echo "Pulling required Docker images..." >>"$LOGFILE" +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +echo "All images pulled successfully." >>"$LOGFILE" + # Stop and remove existing Coolify containers to prevent conflicts # This handles both old installations (project "source") and new ones (project "coolify") echo "Stopping existing Coolify containers..." >>"$LOGFILE" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 36746679b..3ce426548 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -64,6 +64,17 @@ if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi +# Pull all required images before stopping containers +# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.) +echo "Pulling required Docker images..." >>"$LOGFILE" +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +echo "All images pulled successfully." >>"$LOGFILE" + # Stop and remove existing Coolify containers to prevent conflicts # This handles both old installations (project "source") and new ones (project "coolify") echo "Stopping existing Coolify containers..." >>"$LOGFILE" From 6a9027dcbf52ffe1711ce3be43928d9f675237b0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:18:57 +0100 Subject: [PATCH 083/203] Add human-friendly output to upgrade script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show clear progress with numbered steps (1/6 through 6/6) - Display header and footer banners - Show individual image pull progress - Show which containers are being stopped - Display final success message with version and log location - Keep detailed logging to file for debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Actions/Server/UpdateCoolify.php | 12 ++---- scripts/upgrade.sh | 64 ++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index a26e7daaa..b5ebd92b2 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -30,7 +30,6 @@ public function handle($manual_update = false) if (! $this->server) { return; } - CleanupDocker::dispatch($this->server, false, false); // Fetch fresh version from CDN instead of using cache try { @@ -117,17 +116,12 @@ public function handle($manual_update = false) private function update() { - $helperImage = config('constants.coolify.helper_image'); - $latest_version = getHelperVersion(); - instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); - - $image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion; - instant_remote_process(["docker pull -q $image"], $this->server, false); - + $latestHelperImageVersion = getHelperVersion(); $upgradeScriptUrl = config('constants.coolify.upgrade_script_url'); + remote_process([ "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh", - "bash /data/coolify/source/upgrade.sh $this->latestVersion", + "bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion", ], $this->server); } } diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 3ce426548..73514214f 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -11,13 +11,22 @@ ENV_FILE="/data/coolify/source/.env" DATE=$(date +%Y-%m-%d-%H-%M-%S) LOGFILE="/data/coolify/source/upgrade-${DATE}.log" +echo "" +echo "==========================================" +echo " Coolify Upgrade - ${DATE}" +echo "==========================================" +echo "" + +echo "1/6 Downloading latest configuration files..." curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production +echo " Done." # Backup existing .env file before making any changes if [ "$SKIP_BACKUP" != "true" ]; then if [ -f "$ENV_FILE" ]; then + echo " Creating backup of .env file..." echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE" cp "$ENV_FILE" "$ENV_FILE-$DATE" else @@ -25,6 +34,8 @@ if [ "$SKIP_BACKUP" != "true" ]; then fi fi +echo "" +echo "2/6 Updating environment configuration..." echo "Merging .env.production values into .env" >>"$LOGFILE" awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" echo ".env file merged successfully" >>"$LOGFILE" @@ -48,12 +59,13 @@ echo "Checking and updating environment variables if necessary..." >>"$LOGFILE" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" +echo " Done." # Make sure coolify network exists # It is created when starting Coolify with docker compose if ! docker network inspect coolify >/dev/null 2>&1; then if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then - echo "Failed to create coolify network with ipv6. Trying without ipv6..." + echo "Failed to create coolify network with ipv6. Trying without ipv6..." >>"$LOGFILE" docker network create --attachable coolify 2>/dev/null fi fi @@ -64,31 +76,59 @@ if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi -# Pull all required images before stopping containers -# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.) +echo "" +echo "3/6 Pulling Docker images..." +echo " This may take a few minutes depending on your connection." echo "Pulling required Docker images..." >>"$LOGFILE" -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } -docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; } -docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; } -# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; } -echo "All images pulled successfully." >>"$LOGFILE" -# Stop and remove existing Coolify containers to prevent conflicts -# This handles both old installations (project "source") and new ones (project "coolify") +echo " - Pulling Coolify image..." +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull Coolify image. Aborting upgrade."; echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } + +echo " - Pulling Coolify helper image..." +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull helper image. Aborting upgrade."; echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } + +echo " - Pulling PostgreSQL image..." +docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade."; echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; } + +echo " - Pulling Redis image..." +docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull Redis image. Aborting upgrade."; echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; } + +echo " - Pulling Coolify realtime image..." +docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull realtime image. Aborting upgrade."; echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; } + +echo "All images pulled successfully." >>"$LOGFILE" +echo " All images pulled successfully." + +echo "" +echo "4/6 Stopping existing containers..." echo "Stopping existing Coolify containers..." >>"$LOGFILE" for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then + echo " - Stopping ${container}..." docker stop "$container" >>"$LOGFILE" 2>&1 || true docker rm "$container" >>"$LOGFILE" 2>&1 || true echo " - Removed container: $container" >>"$LOGFILE" fi done +echo " Done." +echo "" +echo "5/6 Starting new containers..." if [ -f /data/coolify/source/docker-compose.custom.yml ]; then + echo " Custom docker-compose.yml detected." echo "docker-compose.custom.yml detected." >>"$LOGFILE" docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 else docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 fi +echo " Done." + +echo "" +echo "6/6 Upgrade complete!" +echo "" +echo "==========================================" +echo " Coolify has been upgraded to ${LATEST_IMAGE}" +echo "==========================================" +echo "" +echo " Log file: ${LOGFILE}" +echo "" From 7dc93001e3e4d1a089a7dcac3120b4e68669582d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:32:49 +0100 Subject: [PATCH 084/203] Improve log file format with timestamps and sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add log() helper for timestamped entries - Add log_section() for clear section headers - Include upgrade metadata at start (version, registry, etc.) - Log each step with clear descriptions - Add completion timestamp at end - Track container operations individually 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/upgrade.sh | 129 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 73514214f..f922983c6 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -11,34 +11,63 @@ ENV_FILE="/data/coolify/source/.env" DATE=$(date +%Y-%m-%d-%H-%M-%S) LOGFILE="/data/coolify/source/upgrade-${DATE}.log" +# Helper function to log with timestamp +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE" +} + +# Helper function to log section headers +log_section() { + echo "" >>"$LOGFILE" + echo "============================================================" >>"$LOGFILE" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE" + echo "============================================================" >>"$LOGFILE" +} + echo "" echo "==========================================" echo " Coolify Upgrade - ${DATE}" echo "==========================================" echo "" +# Initialize log file with header +echo "============================================================" >>"$LOGFILE" +echo "Coolify Upgrade Log" >>"$LOGFILE" +echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" +echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE" +echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE" +echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE" +echo "============================================================" >>"$LOGFILE" + +log_section "Step 1/6: 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 +log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml" curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml +log "Downloading .env.production from ${CDN}/.env.production" curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production +log "Configuration files downloaded successfully" echo " Done." # Backup existing .env file before making any changes if [ "$SKIP_BACKUP" != "true" ]; then if [ -f "$ENV_FILE" ]; then echo " Creating backup of .env file..." - echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE" + log "Creating backup of .env file to .env-$DATE" cp "$ENV_FILE" "$ENV_FILE-$DATE" + log "Backup created: ${ENV_FILE}-${DATE}" else - echo "No existing .env file found to backup" >>"$LOGFILE" + log "WARNING: No existing .env file found to backup" fi fi +log_section "Step 2/6: Updating environment configuration" echo "" echo "2/6 Updating environment configuration..." -echo "Merging .env.production values into .env" >>"$LOGFILE" +log "Merging .env.production values into .env" awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" -echo ".env file merged successfully" >>"$LOGFILE" +log "Environment file merged successfully" update_env_var() { local key="$1" @@ -47,82 +76,140 @@ update_env_var() { # If variable "key=" exists but has no value, update the value of the existing line if grep -q "^${key}=$" "$ENV_FILE"; then sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE" - echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE" + log "Updated ${key} (was empty)" # If variable "key=" doesn't exist, append it to the file with value elif ! grep -q "^${key}=" "$ENV_FILE"; then printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" - echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE" + log "Added ${key} (was missing)" fi } -echo "Checking and updating environment variables if necessary..." >>"$LOGFILE" +log "Checking environment variables..." update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" +log "Environment variables check complete" echo " Done." # Make sure coolify network exists # It is created when starting Coolify with docker compose +log "Checking Docker network 'coolify'..." if ! docker network inspect coolify >/dev/null 2>&1; then + log "Network 'coolify' does not exist, creating..." if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then - echo "Failed to create coolify network with ipv6. Trying without ipv6..." >>"$LOGFILE" + log "Failed to create network with IPv6, trying without IPv6..." docker network create --attachable coolify 2>/dev/null + log "Network 'coolify' created without IPv6" + else + log "Network 'coolify' created with IPv6 support" fi +else + log "Network 'coolify' already exists" fi # Check if Docker config file exists DOCKER_CONFIG_MOUNT="" if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" + log "Docker config mount enabled: /root/.docker/config.json" fi +log_section "Step 3/6: Pulling Docker images" echo "" echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." -echo "Pulling required Docker images..." >>"$LOGFILE" echo " - Pulling Coolify image..." -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull Coolify image. Aborting upgrade."; echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" +if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1; then + log "Successfully pulled Coolify image" +else + log "ERROR: Failed to pull Coolify image" + echo " ERROR: Failed to pull Coolify image. Aborting upgrade." + exit 1 +fi echo " - Pulling Coolify helper image..." -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull helper image. Aborting upgrade."; echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" +if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1; then + log "Successfully pulled Coolify helper image" +else + log "ERROR: Failed to pull Coolify helper image" + echo " ERROR: Failed to pull helper image. Aborting upgrade." + exit 1 +fi echo " - Pulling PostgreSQL image..." -docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade."; echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +log "Pulling image: postgres:15-alpine" +if docker pull postgres:15-alpine >>"$LOGFILE" 2>&1; then + log "Successfully pulled PostgreSQL image" +else + log "ERROR: Failed to pull PostgreSQL image" + echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade." + exit 1 +fi echo " - Pulling Redis image..." -docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull Redis image. Aborting upgrade."; echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +log "Pulling image: redis:7-alpine" +if docker pull redis:7-alpine >>"$LOGFILE" 2>&1; then + log "Successfully pulled Redis image" +else + log "ERROR: Failed to pull Redis image" + echo " ERROR: Failed to pull Redis image. Aborting upgrade." + exit 1 +fi echo " - Pulling Coolify realtime image..." -docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo " ERROR: Failed to pull realtime image. Aborting upgrade."; echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; } +log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" +if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1; then + log "Successfully pulled Coolify realtime image" +else + log "ERROR: Failed to pull Coolify realtime image" + echo " ERROR: Failed to pull realtime image. Aborting upgrade." + exit 1 +fi -echo "All images pulled successfully." >>"$LOGFILE" +log "All images pulled successfully" echo " All images pulled successfully." +log_section "Step 4/6: Stopping existing containers" echo "" echo "4/6 Stopping existing containers..." -echo "Stopping existing Coolify containers..." >>"$LOGFILE" for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then echo " - Stopping ${container}..." + log "Stopping container: ${container}" docker stop "$container" >>"$LOGFILE" 2>&1 || true + log "Removing container: ${container}" docker rm "$container" >>"$LOGFILE" 2>&1 || true - echo " - Removed container: $container" >>"$LOGFILE" + log "Container ${container} stopped and removed" + else + log "Container ${container} not found (skipping)" fi done +log "Container cleanup complete" echo " Done." +log_section "Step 5/6: Starting new containers" echo "" echo "5/6 Starting new containers..." if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo " Custom docker-compose.yml detected." - echo "docker-compose.custom.yml detected." >>"$LOGFILE" + log "Using custom docker-compose.yml" + log "Running docker compose up with custom configuration..." docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 else + log "Using standard docker-compose configuration" + log "Running docker compose up..." docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 fi +log "Docker compose up completed" echo " Done." +log_section "Step 6/6: Upgrade complete" +log "Coolify upgrade completed successfully" +log "Version: ${LATEST_IMAGE}" + echo "" echo "6/6 Upgrade complete!" echo "" @@ -132,3 +219,9 @@ echo "==========================================" echo "" echo " Log file: ${LOGFILE}" echo "" + +# Final log entry +echo "" >>"$LOGFILE" +echo "============================================================" >>"$LOGFILE" +echo "Upgrade completed: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" +echo "============================================================" >>"$LOGFILE" From f3ccacb2da6da8b502bcb472bfb0bc6a7c776068 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:36:01 +0100 Subject: [PATCH 085/203] Stop coolify container last during upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder container stop sequence to stop dependencies first (db, redis, realtime) before stopping the main coolify container. This prevents the upgrade process from being interrupted when triggered from Coolify UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- other/nightly/upgrade.sh | 3 ++- scripts/upgrade.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index aa0c82865..ba21c82ff 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -77,8 +77,9 @@ echo "All images pulled successfully." >>"$LOGFILE" # Stop and remove existing Coolify containers to prevent conflicts # This handles both old installations (project "source") and new ones (project "coolify") +# Stop coolify last to allow upgrade process to complete gracefully echo "Stopping existing Coolify containers..." >>"$LOGFILE" -for container in coolify coolify-db coolify-redis coolify-realtime; do +for container in coolify-db coolify-redis coolify-realtime coolify; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then docker stop "$container" >>"$LOGFILE" 2>&1 || true docker rm "$container" >>"$LOGFILE" 2>&1 || true diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index f922983c6..204240bb6 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -175,7 +175,8 @@ echo " All images pulled successfully." log_section "Step 4/6: Stopping existing containers" echo "" echo "4/6 Stopping existing containers..." -for container in coolify coolify-db coolify-redis coolify-realtime; do +# Stop coolify last to allow upgrade process to complete gracefully +for container in coolify-db coolify-redis coolify-realtime coolify; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then echo " - Stopping ${container}..." log "Stopping container: ${container}" From f4dbae180536f897185c7933f1904e8b9d39efff Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:39:08 +0100 Subject: [PATCH 086/203] Revert container stop order to original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- other/nightly/upgrade.sh | 3 +-- scripts/upgrade.sh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index ba21c82ff..aa0c82865 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -77,9 +77,8 @@ echo "All images pulled successfully." >>"$LOGFILE" # Stop and remove existing Coolify containers to prevent conflicts # This handles both old installations (project "source") and new ones (project "coolify") -# Stop coolify last to allow upgrade process to complete gracefully echo "Stopping existing Coolify containers..." >>"$LOGFILE" -for container in coolify-db coolify-redis coolify-realtime coolify; do +for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then docker stop "$container" >>"$LOGFILE" 2>&1 || true docker rm "$container" >>"$LOGFILE" 2>&1 || true diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 204240bb6..f922983c6 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -175,8 +175,7 @@ echo " All images pulled successfully." log_section "Step 4/6: Stopping existing containers" echo "" echo "4/6 Stopping existing containers..." -# Stop coolify last to allow upgrade process to complete gracefully -for container in coolify-db coolify-redis coolify-realtime coolify; do +for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then echo " - Stopping ${container}..." log "Stopping container: ${container}" From 1f7888f515da8d67ebad655c35e38ed544cc0543 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:41:09 +0100 Subject: [PATCH 087/203] Use nohup for container restart to survive SSH disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When upgrade is triggered from Coolify UI, the SSH connection is lost when the coolify container stops. Using nohup ensures the container stop/start sequence continues in the background even after the connection drops. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- other/nightly/upgrade.sh | 42 ++++++++++----- scripts/upgrade.sh | 113 ++++++++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 57 deletions(-) diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index aa0c82865..0d3896647 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -77,18 +77,32 @@ echo "All images pulled successfully." >>"$LOGFILE" # Stop and remove existing Coolify containers to prevent conflicts # This handles both old installations (project "source") and new ones (project "coolify") -echo "Stopping existing Coolify containers..." >>"$LOGFILE" -for container in coolify coolify-db coolify-redis coolify-realtime; do - if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then - docker stop "$container" >>"$LOGFILE" 2>&1 || true - docker rm "$container" >>"$LOGFILE" 2>&1 || true - echo " - Removed container: $container" >>"$LOGFILE" - fi -done +# Use nohup to ensure the script continues even if SSH connection is lost +echo "Starting container restart sequence (detached)..." >>"$LOGFILE" -if [ -f /data/coolify/source/docker-compose.custom.yml ]; then - echo "docker-compose.custom.yml detected." >>"$LOGFILE" - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 -else - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 -fi +nohup bash -c " + LOGFILE='$LOGFILE' + DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT' + REGISTRY_URL='$REGISTRY_URL' + LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION' + LATEST_IMAGE='$LATEST_IMAGE' + + # Stop and remove containers + echo 'Stopping existing Coolify containers...' >>\"\$LOGFILE\" + for container in coolify coolify-db coolify-redis coolify-realtime; do + if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then + docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true + docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true + echo \" - Removed container: \$container\" >>\"\$LOGFILE\" + fi + done + + # Start new containers + if [ -f /data/coolify/source/docker-compose.custom.yml ]; then + echo 'docker-compose.custom.yml detected.' >>\"\$LOGFILE\" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + else + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + fi + echo 'Upgrade completed.' >>\"\$LOGFILE\" +" >>"$LOGFILE" 2>&1 & diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index f922983c6..97073712a 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -172,56 +172,83 @@ fi log "All images pulled successfully" echo " All images pulled successfully." -log_section "Step 4/6: Stopping existing containers" +log_section "Step 4/6: Stopping and restarting containers" echo "" -echo "4/6 Stopping existing containers..." -for container in coolify coolify-db coolify-redis coolify-realtime; do - if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then - echo " - Stopping ${container}..." - log "Stopping container: ${container}" - docker stop "$container" >>"$LOGFILE" 2>&1 || true - log "Removing container: ${container}" - docker rm "$container" >>"$LOGFILE" 2>&1 || true - log "Container ${container} stopped and removed" +echo "4/6 Stopping containers and starting new ones..." +echo " This step will restart all Coolify containers." +echo " Check the log file for details: ${LOGFILE}" + +# From this point forward, we need to ensure the script continues even if +# the SSH connection is lost (which happens when coolify container stops) +# We use a subshell with nohup to ensure completion +log "Starting container restart sequence (detached)..." + +nohup bash -c " + LOGFILE='$LOGFILE' + DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT' + REGISTRY_URL='$REGISTRY_URL' + LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION' + LATEST_IMAGE='$LATEST_IMAGE' + + log() { + echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\" + } + + # 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 + log \"Stopping container: \${container}\" + docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true + log \"Removing container: \${container}\" + docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true + log \"Container \${container} stopped and removed\" + else + log \"Container \${container} not found (skipping)\" + fi + done + log \"Container cleanup complete\" + + # Start new containers + echo '' >>\"\$LOGFILE\" + echo '============================================================' >>\"\$LOGFILE\" + log 'Step 5/6: Starting new containers' + echo '============================================================' >>\"\$LOGFILE\" + + if [ -f /data/coolify/source/docker-compose.custom.yml ]; then + log 'Using custom docker-compose.yml' + log 'Running docker compose up with custom configuration...' + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 else - log "Container ${container} not found (skipping)" + log 'Using standard docker-compose configuration' + log 'Running docker compose up...' + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 fi -done -log "Container cleanup complete" -echo " Done." + log 'Docker compose up completed' -log_section "Step 5/6: Starting new containers" + # Final log entry + echo '' >>\"\$LOGFILE\" + echo '============================================================' >>\"\$LOGFILE\" + log 'Step 6/6: Upgrade complete' + echo '============================================================' >>\"\$LOGFILE\" + log 'Coolify upgrade completed successfully' + log 'Version: \${LATEST_IMAGE}' + echo '' >>\"\$LOGFILE\" + echo '============================================================' >>\"\$LOGFILE\" + echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\" + echo '============================================================' >>\"\$LOGFILE\" +" >>"$LOGFILE" 2>&1 & + +# Give the background process a moment to start +sleep 2 +log "Container restart sequence started in background (PID: $!)" echo "" -echo "5/6 Starting new containers..." -if [ -f /data/coolify/source/docker-compose.custom.yml ]; then - echo " Custom docker-compose.yml detected." - log "Using custom docker-compose.yml" - log "Running docker compose up with custom configuration..." - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 -else - log "Using standard docker-compose configuration" - log "Running docker compose up..." - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1 -fi -log "Docker compose up completed" -echo " Done." - -log_section "Step 6/6: Upgrade complete" -log "Coolify upgrade completed successfully" -log "Version: ${LATEST_IMAGE}" - -echo "" -echo "6/6 Upgrade complete!" +echo "5/6 Containers are being restarted in the background..." +echo "6/6 Upgrade process initiated!" echo "" echo "==========================================" -echo " Coolify has been upgraded to ${LATEST_IMAGE}" +echo " Coolify upgrade to ${LATEST_IMAGE} in progress" echo "==========================================" echo "" +echo " The upgrade will continue in the background." +echo " Coolify will be available again shortly." echo " Log file: ${LOGFILE}" -echo "" - -# Final log entry -echo "" >>"$LOGFILE" -echo "============================================================" >>"$LOGFILE" -echo "Upgrade completed: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" -echo "============================================================" >>"$LOGFILE" From 30ac4e079c9b8e23401ae7b6a3594f5c5e19e6a4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:22:01 +0100 Subject: [PATCH 088/203] Fix variable expansion in upgrade log message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use double quotes for LATEST_IMAGE variable in log output so it expands correctly inside the nohup subshell. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/upgrade.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 97073712a..b06cbc412 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -221,7 +221,7 @@ nohup bash -c " else log 'Using standard docker-compose configuration' log 'Running docker compose up...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 fi log 'Docker compose up completed' @@ -231,7 +231,7 @@ nohup bash -c " log 'Step 6/6: Upgrade complete' echo '============================================================' >>\"\$LOGFILE\" log 'Coolify upgrade completed successfully' - log 'Version: \${LATEST_IMAGE}' + log \"Version: \${LATEST_IMAGE}\" echo '' >>\"\$LOGFILE\" echo '============================================================' >>\"\$LOGFILE\" echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\" From 92326c09ea28af6abf427b32a256dbd997ad7133 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:26:08 +0100 Subject: [PATCH 089/203] Improve upgrade process UX with better progress visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add step-by-step progress indicator (Preparing → Helper → Image → Restart) - Display elapsed time during upgrade (MM:SS format) - Show version transition in header (v4.0.0-beta.454 → v4.0.0-beta.456) - Add expandable changelog preview before upgrading - Reduce reload delay from 5s to 3s with countdown timer - Add "Reload Now" button to skip countdown - Improve status messages with step-specific descriptions - Add success state with clear indication when upgrade completes - Create new upgrade-progress component for visual step tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- app/Livewire/Upgrade.php | 37 +++ .../components/upgrade-progress.blade.php | 143 ++++++++++ resources/views/livewire/upgrade.blade.php | 266 ++++++++++++++---- 3 files changed, 398 insertions(+), 48 deletions(-) create mode 100644 resources/views/components/upgrade-progress.blade.php diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index f13baa7a7..1b145b244 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -4,6 +4,7 @@ use App\Actions\Server\UpdateCoolify; use App\Models\InstanceSettings; +use App\Services\ChangelogService; use Livewire\Component; class Upgrade extends Component @@ -14,21 +15,57 @@ class Upgrade extends Component public string $latestVersion = ''; + public string $currentVersion = ''; + + public array $changelogEntries = []; + protected $listeners = ['updateAvailable' => 'checkUpdate']; + public function mount() + { + $this->currentVersion = config('constants.coolify.version'); + } + public function checkUpdate() { try { $this->latestVersion = get_latest_version_of_coolify(); + $this->currentVersion = config('constants.coolify.version'); $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false); if (isDev()) { $this->isUpgradeAvailable = true; } + $this->loadChangelog(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function loadChangelog() + { + try { + $service = app(ChangelogService::class); + $currentVersion = str_replace('v', '', $this->currentVersion); + + $this->changelogEntries = $service->getEntries(1) + ->filter(function ($entry) use ($currentVersion) { + $entryVersion = str_replace('v', '', $entry->tag_name); + + return version_compare($entryVersion, $currentVersion, '>'); + }) + ->take(3) + ->map(fn ($entry) => [ + 'tag_name' => $entry->tag_name, + 'title' => $entry->title, + 'content_html' => $entry->content_html, + ]) + ->values() + ->toArray(); + } catch (\Throwable $e) { + $this->changelogEntries = []; + } + } + public function upgrade() { try { diff --git a/resources/views/components/upgrade-progress.blade.php b/resources/views/components/upgrade-progress.blade.php new file mode 100644 index 000000000..13eca4f5b --- /dev/null +++ b/resources/views/components/upgrade-progress.blade.php @@ -0,0 +1,143 @@ +@props(['step' => 0]) + +
+
+ {{-- Step 1: Preparing --}} +
+
+
+ + + +
+ Preparing +
+
+
+ + {{-- Step 2: Helper --}} +
+
+
+ + + +
+ Helper +
+
+
+ + {{-- Step 3: Image --}} +
+
+
+ + + +
+ Image +
+
+
+ + {{-- Step 4: Restart --}} +
+
+
+ + + +
+ Restart +
+
+
+
diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index 37e43935d..101c4cf94 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -1,5 +1,8 @@
+ x-init="$wire.checkUpdate" x-data="upgradeModal({ + currentVersion: @js($currentVersion), + latestVersion: @js($latestVersion) + })"> @if ($isUpgradeAvailable)
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300"> + + {{-- Header --}}
-

Upgrade confirmation

+
+

+
+ {{ $currentVersion }} {{ $latestVersion }} +
+
-
-

Are you sure you would like to upgrade your instance to {{ $latestVersion }}?

-
- -

Any deployments running during the update process will - fail. Please ensure no deployments are in progress on any server before continuing. -

-
-
-

You can review the changelogs here.

-
-

If something goes wrong and you cannot upgrade your instance, You can check the following - guide on what to do. -

-
-

Progress

-
-
+ {{-- Content --}} +
+ {{-- Progress View --}} + + + {{-- Confirmation View --}} +
+ + {{-- Footer Actions --}}
Cancel
- Continue + + Upgrade Now
@@ -89,23 +185,57 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel From 0aa7e376b29b7b912dc771a2c237a8cd8c65d7eb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:41:14 +0100 Subject: [PATCH 090/203] Simplify upgrade modal and improve help text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove changelog preview section to streamline the UI - Simplify warning message - Add reference to upgrade logs location on server - Minor formatting improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- resources/views/livewire/upgrade.blade.php | 87 ++++++++-------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index 101c4cf94..eedadbb56 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -8,17 +8,15 @@ @@ -81,14 +80,21 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
@@ -99,7 +105,8 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 @@ -168,6 +188,7 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel elapsedTime: 0, currentStep: 0, upgradeComplete: false, + upgradeError: false, successCountdown: 3, currentVersion: config.currentVersion || '', latestVersion: config.latestVersion || '', @@ -178,12 +199,16 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel this.currentStep = 1; this.currentStatus = 'Starting upgrade...'; this.startTimer(); + // Trigger server-side upgrade script via Livewire this.$wire.$call('upgrade'); + // Start client-side status polling this.upgrade(); - window.addEventListener('beforeunload', (event) => { + // Prevent accidental navigation during upgrade + this.beforeUnloadHandler = (event) => { event.preventDefault(); event.returnValue = ''; - }); + }; + window.addEventListener('beforeunload', this.beforeUnloadHandler); }, startTimer() { @@ -259,6 +284,11 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel clearInterval(this.elapsedInterval); this.elapsedInterval = null; } + // Remove beforeunload handler now that upgrade is complete + if (this.beforeUnloadHandler) { + window.removeEventListener('beforeunload', this.beforeUnloadHandler); + this.beforeUnloadHandler = null; + } this.upgradeComplete = true; this.currentStep = 5; @@ -278,6 +308,38 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel window.location.reload(); }, + showError(message) { + // Stop all intervals + if (this.checkHealthInterval) { + clearInterval(this.checkHealthInterval); + this.checkHealthInterval = null; + } + if (this.checkUpgradeStatusInterval) { + clearInterval(this.checkUpgradeStatusInterval); + this.checkUpgradeStatusInterval = null; + } + if (this.elapsedInterval) { + clearInterval(this.elapsedInterval); + this.elapsedInterval = null; + } + // Remove beforeunload handler so user can close modal + if (this.beforeUnloadHandler) { + window.removeEventListener('beforeunload', this.beforeUnloadHandler); + this.beforeUnloadHandler = null; + } + + this.upgradeError = true; + this.currentStatus = `Error: ${message}`; + }, + + closeErrorModal() { + this.modalOpen = false; + this.showProgress = false; + this.upgradeError = false; + this.currentStatus = ''; + this.currentStep = 0; + }, + upgrade() { if (this.checkUpgradeStatusInterval) return true; this.currentStep = 1; @@ -294,7 +356,7 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel } else if (data.status === 'complete') { this.showSuccess(); } else if (data.status === 'error') { - this.currentStatus = `Error: ${data.message}`; + this.showError(data.message); } } catch (error) { // Service is down - switch to health check mode diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 8ade89669..ad3f32009 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -244,7 +244,7 @@ nohup bash -c " else log 'Using standard docker-compose configuration' log 'Running docker compose up...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 fi log 'Docker compose up completed' From c6945c86eadefd95e676ca22ccb1953180882695 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:12:33 +0100 Subject: [PATCH 101/203] Parse Docker images dynamically from docker-compose files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded image pulls (postgres:15-alpine, redis:7-alpine, coolify-realtime:1.0.10) with dynamic extraction using `docker compose config --images`. This ensures the upgrade script automatically handles new services and version changes without manual updates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/upgrade.sh | 99 +++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index ad3f32009..4259aded2 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -59,6 +59,30 @@ curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production log "Configuration files downloaded successfully" echo " Done." +# Extract all images from docker-compose configuration +log "Extracting all images from docker-compose configuration..." +COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml" + +# Check if custom compose file exists +if [ -f /data/coolify/source/docker-compose.custom.yml ]; then + COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml" + log "Including custom docker-compose.yml in image extraction" +fi + +# Get all unique images from docker compose config +# LATEST_IMAGE env var is needed for image substitution in compose files +IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) + +if [ -z "$IMAGES" ]; then + log "ERROR: Failed to extract images from docker-compose files" + write_status "error" "Failed to parse docker-compose configuration" + echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade." + exit 1 +fi + +log "Images to pull:" +echo "$IMAGES" | while read img; do log " - $img"; done + # Backup existing .env file before making any changes if [ "$SKIP_BACKUP" != "true" ]; then if [ -f "$ENV_FILE" ]; then @@ -130,60 +154,35 @@ echo "" echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." -echo " - Pulling Coolify image..." -log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" -if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1; then - log "Successfully pulled Coolify image" +# Also pull the helper image (not in compose files but needed for upgrade) +HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" +echo " - Pulling $HELPER_IMAGE..." +log "Pulling image: $HELPER_IMAGE" +if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then + log "Successfully pulled $HELPER_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." + log "ERROR: Failed to pull $HELPER_IMAGE" + write_status "error" "Failed to pull $HELPER_IMAGE" + echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade." exit 1 fi -echo " - Pulling Coolify helper image..." -log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" -if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1; then - 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 - -echo " - Pulling PostgreSQL image..." -log "Pulling image: postgres:15-alpine" -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 - -echo " - Pulling Redis image..." -log "Pulling image: redis:7-alpine" -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 - -echo " - Pulling Coolify realtime image..." -log "Pulling image: ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" -if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1; then - 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 +# Pull all images from compose config +# Using a for loop to avoid subshell issues with exit +for IMAGE in $IMAGES; do + if [ -n "$IMAGE" ]; then + echo " - Pulling $IMAGE..." + log "Pulling image: $IMAGE" + if docker pull "$IMAGE" >>"$LOGFILE" 2>&1; then + log "Successfully pulled $IMAGE" + else + log "ERROR: Failed to pull $IMAGE" + write_status "error" "Failed to pull $IMAGE" + echo " ERROR: Failed to pull $IMAGE. Aborting upgrade." + exit 1 + fi + fi +done log "All images pulled successfully" echo " All images pulled successfully." From 918959b7f5a3de05732fa55a455697ce171845f2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:26:31 +0100 Subject: [PATCH 102/203] Remove unused ChangelogService import from Upgrade component --- app/Livewire/Upgrade.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 37cc8ec28..36bee2a23 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -5,7 +5,6 @@ use App\Actions\Server\UpdateCoolify; use App\Models\InstanceSettings; use App\Models\Server; -use App\Services\ChangelogService; use Livewire\Component; class Upgrade extends Component From 6fe4ebeb7e2d0887e6a0c9997fa25c810d2fa8bc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:22:11 +0100 Subject: [PATCH 103/203] Refactor docker run commands in upgrade script to remove project name specification --- scripts/upgrade.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 4259aded2..648849d5c 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -239,11 +239,11 @@ nohup bash -c " if [ -f /data/coolify/source/docker-compose.custom.yml ]; then log 'Using custom docker-compose.yml' log 'Running docker compose up with custom configuration...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 else log 'Using standard docker-compose configuration' log 'Running docker compose up...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 fi log 'Docker compose up completed' From 8d20d509374564d7436635ba555d55c8b41f74bc Mon Sep 17 00:00:00 2001 From: Kyle Essenmacher Date: Sun, 14 Dec 2025 14:07:29 -0500 Subject: [PATCH 104/203] feat: add Escape key support to exit fullscreen logs view --- .../project/shared/get-logs.blade.php | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 8504a160f..ae0896d7a 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -1,22 +1,28 @@
-
isScrolling: false, toggleScroll() { this.alwaysScroll = !this.alwaysScroll; @@ -375,4 +381,4 @@ class="font-mono whitespace-pre-wrap break-all max-w-full text-neutral-400">No l
-
\ No newline at end of file +
From f0dc00f2074e7e3ebbf370430a58e46a2a6f3ad2 Mon Sep 17 00:00:00 2001 From: Lukas <20421914+lumoe@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:24:43 +0100 Subject: [PATCH 105/203] Add STORE_MODEL_IN_DB to env variables --- templates/compose/litellm.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/litellm.yaml b/templates/compose/litellm.yaml index 388541519..9fd0a234b 100644 --- a/templates/compose/litellm.yaml +++ b/templates/compose/litellm.yaml @@ -32,6 +32,7 @@ services: - ANTHROPIC_API_BASE=${ANTHROPIC_API_BASE} - VOYAGE_API_KEY=${VOYAGE_API_KEY} - VOYAGE_API_BASE=${VOYAGE_API_BASE} + - STORE_MODEL_IN_DB=${STORE_MODEL_IN_DB} volumes: - type: bind source: ./litellm-config.yaml From 36d7844989a0bbd97b38e8d4777dc75ed61ed189 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:47:03 +0100 Subject: [PATCH 106/203] Fix deployment log view UX issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prevent text selection from being cleared when logs are re-rendered during polling - Preserve fullscreen state when toggling debug logs or other Livewire updates - Fix log filtering to properly apply when debug mode is toggled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- .../Project/Application/Deployment/Show.php | 2 + .../application/deployment/show.blade.php | 57 ++++++++++++------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 44ab419c2..8c0ee1a3f 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -20,6 +20,8 @@ class Show extends Component public bool $is_debug_enabled = false; + public bool $fullscreen = false; + private bool $deploymentFinishedDispatched = false; public function getListeners() diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index f2cde05cf..f125cde91 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -6,7 +6,7 @@
- @if (data_get($application_deployment_queue, 'status') === 'in_progress') -
Deployment is -
- {{ Str::headline(data_get($this->application_deployment_queue, 'status')) }}. -
- -
- {{--
Logs will be updated automatically.
--}} - @else -
Deployment is {{ Str::headline(data_get($application_deployment_queue, 'status')) }}. -
- @endif -
+
+ :class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
- - +
+ @if (data_get($application_deployment_queue, 'status') === 'in_progress') +
+ Deployment is + In Progress + +
+ @else +
+ Deployment is + {{ Str::headline(data_get($application_deployment_queue, 'status')) }} +
+ @endif + +
$searchableContent = $line['timestamp'] . ' ' . $lineContent; @endphp
isset($line['command']) && $line['command'], 'flex gap-2', ])> From d40c2caca20113023576e6d711087750532e7a7c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:49:26 +0100 Subject: [PATCH 107/203] Fix text disappearing during selection in deployment logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure initial render happens even when selection is active by checking if element already has content before skipping re-render. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../livewire/project/application/deployment/show.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index f125cde91..365db724d 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -63,7 +63,8 @@ }, renderHighlightedLog(el, text) { // Skip re-render if user has text selected in logs (preserves copy ability) - if (this.hasActiveLogSelection()) { + // But always render if the element is empty (initial render) + if (el.textContent && this.hasActiveLogSelection()) { return; } From 5cc822c9963312dd9f7bd3f5ba8b7a45d5e771e2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:52:00 +0100 Subject: [PATCH 108/203] Fix text selection issue in runtime logs view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same selection preservation fix to the runtime logs component (get-logs.blade.php) that was applied to deployment logs: - Add hasActiveLogSelection() helper to detect active text selection - Skip re-render when user has text selected (preserves copy ability) - Add renderTrigger mechanism to ensure filtering works after refresh - Use x-effect for hidden state to properly react to Livewire updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../project/shared/get-logs.blade.php | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 8504a160f..692138c5b 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -9,6 +9,7 @@ scrollDebounce: null, colorLogs: localStorage.getItem('coolify-color-logs') === 'true', searchQuery: '', + renderTrigger: 0, containerName: '{{ $container ?? "logs" }}', makeFullscreen() { this.fullscreen = !this.fullscreen; @@ -80,6 +81,18 @@ if (!this.searchQuery.trim()) return true; return line.toLowerCase().includes(this.searchQuery.toLowerCase()); }, + hasActiveLogSelection() { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || !selection.toString().trim()) { + return false; + } + const logsContainer = document.getElementById('logs'); + if (!logsContainer) return false; + + // Check if selection is within the logs container + const range = selection.getRangeAt(0); + return logsContainer.contains(range.commonAncestorContainer); + }, decodeHtml(text) { // Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS let decoded = text; @@ -96,6 +109,12 @@ return decoded; }, renderHighlightedLog(el, text) { + // Skip re-render if user has text selected in logs (preserves copy ability) + // But always render if the element is empty (initial render) + if (el.textContent && this.hasActiveLogSelection()) { + return; + } + const decoded = this.decodeHtml(text); el.textContent = ''; @@ -167,6 +186,12 @@ this.$wire.getLogs(true); this.logsLoaded = true; } + // Re-render logs after Livewire updates + Livewire.hook('commit', ({ succeed }) => { + succeed(() => { + this.$nextTick(() => { this.renderTrigger++; }); + }); + }); } }"> @if ($collapsible) @@ -350,8 +375,8 @@ class="text-gray-500 dark:text-gray-400 py-2"> @endphp
{{ $timestamp }} @endif
@endforeach From 6b9c633fe744ab2aa47a3a6341bae775628c8268 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:53:02 +0100 Subject: [PATCH 109/203] Prevent Livewire from morphing logs when text is selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Livewire's morph.updating hook to skip DOM morphing of the logs container when user has text selected. This prevents the selection from being lost when polling or manual refresh occurs. The previous fix only prevented the JavaScript-based re-render, but Livewire's morphing was still replacing the DOM elements entirely. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../livewire/project/application/deployment/show.blade.php | 6 ++++++ resources/views/livewire/project/shared/get-logs.blade.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 365db724d..5f37786f5 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -147,6 +147,12 @@ } }, init() { + // Prevent Livewire from morphing logs container when text is selected + Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => { + if (el.id === 'logs' && this.hasActiveLogSelection()) { + skip(); + } + }); // Re-render logs after Livewire updates document.addEventListener('livewire:navigated', () => { this.$nextTick(() => { this.renderTrigger++; }); diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 692138c5b..91f615227 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -186,6 +186,12 @@ this.$wire.getLogs(true); this.logsLoaded = true; } + // Prevent Livewire from morphing logs container when text is selected + Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => { + if (el.id === 'logs' && this.hasActiveLogSelection()) { + skip(); + } + }); // Re-render logs after Livewire updates Livewire.hook('commit', ({ succeed }) => { succeed(() => { From 987252a179f9ae1575977cb9a86374a63443c501 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:57:07 +0100 Subject: [PATCH 110/203] Move polling button next to refresh button in runtime logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder toolbar buttons so that Refresh and Stream Logs (polling) are adjacent, making the related actions easier to find. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../project/shared/get-logs.blade.php | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 91f615227..c4b610873 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -266,6 +266,23 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /> + - + -
- - Overview - - - Settings - - - All Resources - +
+ @foreach ($projects as $project) + + {{ $project->name }} + + @endforeach
-
  • -
    +
  • +
    {{ data_get($resource, 'environment.name') }} - + - -
    - - All Resources - - - Environment Settings - - - Clone Environment - + +
    + +
    + @foreach ($environments as $environment) + @php + $envResources = collect() + ->merge($environment->applications->map(fn($app) => ['type' => 'application', 'resource' => $app])) + ->merge($environment->databases()->map(fn($db) => ['type' => 'database', 'resource' => $db])) + ->merge($environment->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc])); + @endphp + + @endforeach +
    + + + @foreach ($environments as $environment) + @php + $envResources = collect() + ->merge($environment->applications->map(fn($app) => ['type' => 'application', 'resource' => $app])) + ->merge($environment->databases()->map(fn($db) => ['type' => 'database', 'resource' => $db])) + ->merge($environment->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc])); + @endphp + @if ($envResources->count() > 0) +
    +
    + @foreach ($envResources as $envResource) + @php + $resType = $envResource['type']; + $res = $envResource['resource']; + $resRoute = match($resType) { + 'application' => route('project.application.configuration', [ + 'project_uuid' => $currentProjectUuid, + 'environment_uuid' => $environment->uuid, + 'application_uuid' => $res->uuid, + ]), + 'service' => route('project.service.configuration', [ + 'project_uuid' => $currentProjectUuid, + 'environment_uuid' => $environment->uuid, + 'service_uuid' => $res->uuid, + ]), + 'database' => route('project.database.configuration', [ + 'project_uuid' => $currentProjectUuid, + 'environment_uuid' => $environment->uuid, + 'database_uuid' => $res->uuid, + ]), + }; + $isCurrentResource = $res->uuid === $currentResourceUuid; + @endphp + + @endforeach +
    + + + @foreach ($envResources as $envResource) + @php + $resType = $envResource['type']; + $res = $envResource['resource']; + $resParams = [ + 'project_uuid' => $currentProjectUuid, + 'environment_uuid' => $environment->uuid, + ]; + if ($resType === 'application') { + $resParams['application_uuid'] = $res->uuid; + } elseif ($resType === 'service') { + $resParams['service_uuid'] = $res->uuid; + } else { + $resParams['database_uuid'] = $res->uuid; + } + $resKey = $environment->uuid . '-' . $res->uuid; + @endphp +
    + +
    + @if ($resType === 'application') + + Deployments + Logs + @can('canAccessTerminal') + Terminal + @endcan + @elseif ($resType === 'service') + + Logs + @can('canAccessTerminal') + Terminal + @endcan + @else + + Logs + @can('canAccessTerminal') + Terminal + @endcan + @if ( + $res->getMorphClass() === 'App\Models\StandalonePostgresql' || + $res->getMorphClass() === 'App\Models\StandaloneMongodb' || + $res->getMorphClass() === 'App\Models\StandaloneMysql' || + $res->getMorphClass() === 'App\Models\StandaloneMariadb') + Backups + @endif + @endif +
    + + + +
    + @endforeach +
    + @endif + @endforeach
  • -
  • -
    + @php + $resourceUuid = data_get($resource, 'uuid'); + $resourceType = $resource->getMorphClass(); + $isApplication = $resourceType === 'App\Models\Application'; + $isService = $resourceType === 'App\Models\Service'; + $isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone'); + $routeParams = [ + 'project_uuid' => $currentProjectUuid, + 'environment_uuid' => $currentEnvironmentUuid, + ]; + if ($isApplication) { + $routeParams['application_uuid'] = $resourceUuid; + } elseif ($isService) { + $routeParams['service_uuid'] = $resourceUuid; + } else { + $routeParams['database_uuid'] = $resourceUuid; + } + @endphp +
  • +
    + href="{{ $isApplication + ? route('project.application.configuration', $routeParams) + : ($isService + ? route('project.service.configuration', $routeParams) + : route('project.database.configuration', $routeParams)) }}"> {{ data_get($resource, 'name') }} - + - -
    - @if($resource->getMorphClass() === 'App\Models\Application') - - - Configuration - - - Deployments - - - Logs - - - Terminal - - @elseif(str_contains($resource->getMorphClass(), 'Database')) - - - Configuration - - - Backups - - - Logs - - - Terminal - - @elseif($resource->getMorphClass() === 'App\Models\Service') - - - Configuration - - - Logs - - - Terminal - - @endif + +
    + +
    + @if ($isApplication) + + + + Deployments + + + Logs + + @can('canAccessTerminal') + + Terminal + + @endcan + @elseif ($isService) + + + + Logs + + @can('canAccessTerminal') + + Terminal + + @endcan + @else + + + + Logs + + @can('canAccessTerminal') + + Terminal + + @endcan + @if ( + $resourceType === 'App\Models\StandalonePostgresql' || + $resourceType === 'App\Models\StandaloneMongodb' || + $resourceType === 'App\Models\StandaloneMysql' || + $resourceType === 'App\Models\StandaloneMariadb') + + Backups + + @endif + @endif +
    + + +
  • @@ -251,8 +464,9 @@ class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200"