From 0117e1102f95f0092f4b3c65e58ccc37308c4e90 Mon Sep 17 00:00:00 2001 From: FUTC-Coding Date: Tue, 2 Dec 2025 17:07:24 +0100 Subject: [PATCH 01/43] Update Umami image tag to latest version --- templates/compose/umami.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/umami.yaml b/templates/compose/umami.yaml index d4248704b..55dd4ccde 100644 --- a/templates/compose/umami.yaml +++ b/templates/compose/umami.yaml @@ -7,7 +7,7 @@ services: umami: - image: ghcr.io/umami-software/umami:postgresql-latest + image: ghcr.io/umami-software/umami:latest environment: - SERVICE_URL_UMAMI_3000 - DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB From 421e5078ea867c619b01f876dc3e741c470ee48c Mon Sep 17 00:00:00 2001 From: FUTC-Coding Date: Tue, 2 Dec 2025 19:09:28 +0100 Subject: [PATCH 02/43] Update Umami image version to 3.0.1 --- templates/compose/umami.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/umami.yaml b/templates/compose/umami.yaml index 55dd4ccde..b3251b9e7 100644 --- a/templates/compose/umami.yaml +++ b/templates/compose/umami.yaml @@ -7,7 +7,7 @@ services: umami: - image: ghcr.io/umami-software/umami:latest + image: ghcr.io/umami-software/umami:3.0.1 environment: - SERVICE_URL_UMAMI_3000 - DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB From ab1542e152727eef3016d57f273e19dd839d8244 Mon Sep 17 00:00:00 2001 From: FUTC-Coding Date: Sun, 7 Dec 2025 12:21:14 +0100 Subject: [PATCH 03/43] Update Umami image version to 3.0.2 due to next vulnerability CVE-2025-66478 --- templates/compose/umami.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/umami.yaml b/templates/compose/umami.yaml index b3251b9e7..2b4ed1972 100644 --- a/templates/compose/umami.yaml +++ b/templates/compose/umami.yaml @@ -7,7 +7,7 @@ services: umami: - image: ghcr.io/umami-software/umami:3.0.1 + image: ghcr.io/umami-software/umami:3.0.2 environment: - SERVICE_URL_UMAMI_3000 - DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB From f152ec00ada70757da38e0b789f049b14d813e33 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:18:58 +0100 Subject: [PATCH 04/43] fix: Detect read-only Docker volumes with long-form syntax and enable refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed isReadOnlyVolume() to detect both short-form (:ro) and long-form (read_only: true) Docker Compose volume syntax - Fixed path matching to use mount_path only (fs_path is transformed during parsing from ./file to absolute path) - Added "Load from server" button for read-only volumes to allow users to refresh content - Changed loadStorageOnServer() authorization from 'update' to 'view' since loading is a read operation - Added helper text to Content field warning users that content may be outdated - Applied fixes to both LocalFileVolume and LocalPersistentVolume models 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Service/FileStorage.php | 3 +- app/Models/LocalFileVolume.php | 18 +- app/Models/LocalPersistentVolume.php | 14 ++ .../project/service/file-storage.blade.php | 9 + tests/Unit/LocalFileVolumeReadOnlyTest.php | 219 ++++++++++++++++++ 5 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/LocalFileVolumeReadOnlyTest.php diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2ce4374a0..8a61e3c38 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -104,7 +104,8 @@ public function convertToDirectory() public function loadStorageOnServer() { try { - $this->authorize('update', $this->resource); + // Loading content is a read operation, so we use 'view' permission + $this->authorize('view', $this->resource); $this->fileStorage->loadStorageOnServer(); $this->syncData(); diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index 96170dbd6..dda5de194 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -239,22 +239,32 @@ public function isReadOnlyVolume(): bool $volumes = $compose['services'][$serviceName]['volumes']; // Check each volume to find a match + // Note: We match on mount_path (container path) only, since fs_path gets transformed + // from relative (./file) to absolute (/data/coolify/services/uuid/file) during parsing foreach ($volumes as $volume) { // Volume can be string like "host:container:ro" or "host:container" if (is_string($volume)) { $parts = explode(':', $volume); - // Check if this volume matches our fs_path and mount_path + // Check if this volume matches our mount_path if (count($parts) >= 2) { - $hostPath = $parts[0]; $containerPath = $parts[1]; $options = $parts[2] ?? null; - // Match based on fs_path and mount_path - if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) { + // Match based on mount_path (container path) + if ($containerPath === $this->mount_path) { return $options === 'ro'; } } + } elseif (is_array($volume)) { + // Long-form syntax: { type: bind, source: ..., target: ..., read_only: true } + $containerPath = data_get($volume, 'target'); + $readOnly = data_get($volume, 'read_only', false); + + // Match based on mount_path (container path) + if ($containerPath === $this->mount_path) { + return $readOnly === true; + } } } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index e7862478b..26e9b3e85 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -85,6 +85,7 @@ public function isReadOnlyVolume(): bool $volumes = $compose['services'][$serviceName]['volumes']; // Check each volume to find a match + // Note: We match on mount_path (container path) only, since host paths get transformed foreach ($volumes as $volume) { // Volume can be string like "host:container:ro" or "host:container" if (is_string($volume)) { @@ -104,6 +105,19 @@ public function isReadOnlyVolume(): bool return $options === 'ro'; } } + } elseif (is_array($volume)) { + // Long-form syntax: { type: bind/volume, source: ..., target: ..., read_only: true } + $containerPath = data_get($volume, 'target'); + $readOnly = data_get($volume, 'read_only', false); + + // Match based on mount_path + // Remove leading slash from mount_path if present for comparison + $mountPath = str($this->mount_path)->ltrim('/')->toString(); + $containerPathClean = str($containerPath)->ltrim('/')->toString(); + + if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) { + return $readOnly === true; + } } } diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 4ab966ec3..1dd58fe17 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -65,6 +65,7 @@ @endif @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) @@ -79,12 +80,19 @@ @endif @endcan @endif @else {{-- Read-only view --}} @if (!$fileStorage->is_directory) + @can('view', $resource) +
+ Load from + server +
+ @endcan @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
@endif @endif diff --git a/tests/Unit/LocalFileVolumeReadOnlyTest.php b/tests/Unit/LocalFileVolumeReadOnlyTest.php new file mode 100644 index 000000000..9c2237e62 --- /dev/null +++ b/tests/Unit/LocalFileVolumeReadOnlyTest.php @@ -0,0 +1,219 @@ += 2) { + $containerPath = $parts[1]; + $options = $parts[2] ?? null; + + if ($containerPath === $mountPath) { + return $options === 'ro'; + } + } + } elseif (is_array($volume)) { + // Long-form syntax: { type: bind, source: ..., target: ..., read_only: true } + $containerPath = data_get($volume, 'target'); + $readOnly = data_get($volume, 'read_only', false); + + if ($containerPath === $mountPath) { + return $readOnly === true; + } + } + } + + return false; +} + +test('detects read-only with short-form syntax using :ro', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - ./config.toml:/etc/config.toml:ro +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeTrue(); +}); + +test('detects writable with short-form syntax without :ro', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - ./config.toml:/etc/config.toml +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse(); +}); + +test('detects read-only with long-form syntax and read_only: true', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - type: bind + source: ./garage.toml + target: /etc/garage.toml + read_only: true +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeTrue(); +}); + +test('detects writable with long-form syntax and read_only: false', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - type: bind + source: ./garage.toml + target: /etc/garage.toml + read_only: false +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse(); +}); + +test('detects writable with long-form syntax without read_only key', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - type: bind + source: ./garage.toml + target: /etc/garage.toml +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse(); +}); + +test('handles mixed short-form and long-form volumes in same service', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - ./data:/var/data + - type: bind + source: ./config.toml + target: /etc/config.toml + read_only: true +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/var/data'))->toBeFalse(); + expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeTrue(); +}); + +test('handles same file mounted in multiple services with different read_only settings', function () { + $compose = <<<'YAML' +services: + garage: + image: example/garage + volumes: + - type: bind + source: ./garage.toml + target: /etc/garage.toml + garage-webui: + image: example/webui + volumes: + - type: bind + source: ./garage.toml + target: /etc/garage.toml + read_only: true +YAML; + + // Same file, different services, different read_only status + expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse(); + expect(isVolumeReadOnly($compose, 'garage-webui', '/etc/garage.toml'))->toBeTrue(); +}); + +test('handles volume mount type', function () { + $compose = <<<'YAML' +services: + app: + image: example/app + volumes: + - type: volume + source: mydata + target: /data + read_only: true +YAML; + + expect(isVolumeReadOnly($compose, 'app', '/data'))->toBeTrue(); +}); + +test('returns false when service has no volumes', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse(); +}); + +test('returns false when service does not exist', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - ./config.toml:/etc/config.toml:ro +YAML; + + expect(isVolumeReadOnly($compose, 'nonexistent', '/etc/config.toml'))->toBeFalse(); +}); + +test('returns false when mount path does not match', function () { + $compose = <<<'YAML' +services: + garage: + image: example/image + volumes: + - type: bind + source: ./other.toml + target: /etc/other.toml + read_only: true +YAML; + + expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse(); +}); From 475cfd78cd19d1beecb33bcd80e16c76f544087e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:23:46 +0100 Subject: [PATCH 05/43] fix: Prevent N+1 query in LocalPersistentVolume.isDockerComposeResource() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use relationLoaded() check before accessing the application relationship to avoid triggering individual queries for each volume when rendering storage lists. Update Storage.php to eager load the relationship. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Livewire/Project/Service/Storage.php | 2 +- app/Models/LocalPersistentVolume.php | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 644b100b8..12d8bcbc3 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -67,7 +67,7 @@ public function refreshStoragesFromEvent() public function refreshStorages() { $this->fileStorage = $this->resource->fileStorages()->get(); - $this->resource->refresh(); + $this->resource->load('persistentStorages.resource'); } public function getFilesProperty() diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 26e9b3e85..7126253ea 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -10,6 +10,11 @@ class LocalPersistentVolume extends Model { protected $guarded = []; + public function resource() + { + return $this->morphTo('resource'); + } + public function application() { return $this->morphTo('resource'); @@ -50,6 +55,54 @@ protected function hostPath(): Attribute ); } + // Check if this volume belongs to a service resource + public function isServiceResource(): bool + { + return in_array($this->resource_type, [ + 'App\Models\ServiceApplication', + 'App\Models\ServiceDatabase', + ]); + } + + // Check if this volume belongs to a dockercompose application + public function isDockerComposeResource(): bool + { + if ($this->resource_type !== 'App\Models\Application') { + return false; + } + + // Only access relationship if already eager loaded to avoid N+1 + if (! $this->relationLoaded('resource')) { + return false; + } + + $application = $this->resource; + if (! $application) { + return false; + } + + return data_get($application, 'build_pack') === 'dockercompose'; + } + + // Determine if this volume should be read-only in the UI + // Service volumes and dockercompose application volumes are read-only + // (users should edit compose file directly) + public function shouldBeReadOnlyInUI(): bool + { + // All service volumes should be read-only in UI + if ($this->isServiceResource()) { + return true; + } + + // All dockercompose application volumes should be read-only in UI + if ($this->isDockerComposeResource()) { + return true; + } + + // Check for explicit :ro flag in compose (existing logic) + return $this->isReadOnlyVolume(); + } + // Check if this volume is read-only by parsing the docker-compose content public function isReadOnlyVolume(): bool { From 9bc33d65abd022884ddc6d0e3c463ad4032bb144 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:25:33 +0100 Subject: [PATCH 06/43] fix: Improve read-only volume detection and UI messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isServiceResource() and shouldBeReadOnlyInUI() to LocalFileVolume - Update path matching to handle leading slashes in volume comparisons - Update FileStorage and Show components to use shouldBeReadOnlyInUI() - Show consolidated warning message for service/compose resources in all.blade.php - Remove redundant per-volume warnings for service resources - Clean up configuration.blade.php formatting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Livewire/Project/Service/FileStorage.php | 2 +- app/Livewire/Project/Shared/Storages/Show.php | 2 +- app/Models/LocalFileVolume.php | 33 ++++++++++++++++--- .../project/service/configuration.blade.php | 13 +++----- .../project/service/storage.blade.php | 11 ++----- .../project/shared/storages/all.blade.php | 6 ++++ .../project/shared/storages/show.blade.php | 8 +++-- 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 8a61e3c38..54ef82872 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -62,7 +62,7 @@ public function mount() $this->fs_path = $this->fileStorage->fs_path; } - $this->isReadOnly = $this->fileStorage->isReadOnlyVolume(); + $this->isReadOnly = $this->fileStorage->shouldBeReadOnlyInUI(); $this->syncData(); } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 5970ec904..c8dc68d66 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -67,7 +67,7 @@ private function syncData(bool $toModel = false): void public function mount() { $this->syncData(false); - $this->isReadOnly = $this->storage->isReadOnlyVolume(); + $this->isReadOnly = $this->storage->shouldBeReadOnlyInUI(); } public function submit() diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index dda5de194..9d7095cb5 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -209,6 +209,23 @@ public function scopeWherePlainMountPath($query, $path) return $query->get()->where('plain_mount_path', $path); } + // Check if this volume belongs to a service resource + public function isServiceResource(): bool + { + return in_array($this->resource_type, [ + 'App\Models\ServiceApplication', + 'App\Models\ServiceDatabase', + ]); + } + + // Determine if this volume should be read-only in the UI + // File/directory mounts can be edited even for services + public function shouldBeReadOnlyInUI(): bool + { + // Check for explicit :ro flag in compose (existing logic) + return $this->isReadOnlyVolume(); + } + // Check if this volume is read-only by parsing the docker-compose content public function isReadOnlyVolume(): bool { @@ -251,8 +268,12 @@ public function isReadOnlyVolume(): bool $containerPath = $parts[1]; $options = $parts[2] ?? null; - // Match based on mount_path (container path) - if ($containerPath === $this->mount_path) { + // Match based on mount_path + // Remove leading slash from mount_path if present for comparison + $mountPath = str($this->mount_path)->ltrim('/')->toString(); + $containerPathClean = str($containerPath)->ltrim('/')->toString(); + + if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) { return $options === 'ro'; } } @@ -261,8 +282,12 @@ public function isReadOnlyVolume(): bool $containerPath = data_get($volume, 'target'); $readOnly = data_get($volume, 'read_only', false); - // Match based on mount_path (container path) - if ($containerPath === $this->mount_path) { + // Match based on mount_path + // Remove leading slash from mount_path if present for comparison + $mountPath = str($this->mount_path)->ltrim('/')->toString(); + $containerPathClean = str($containerPath)->ltrim('/')->toString(); + + if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) { return $readOnly === true; } } diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index 7379ca706..f1ad3e06a 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -37,7 +37,7 @@

Services

- @if($applications->isEmpty() && $databases->isEmpty()) + @if ($applications->isEmpty() && $databases->isEmpty())
No services defined in this Docker Compose file.
@@ -76,7 +76,8 @@ @if ($application->fqdn) {{ Str::limit($application->fqdn, 60) }} @can('update', $service) - + @endif -
{{ formatContainerStatus($application->status) }}
+
{{ formatContainerStatus($application->status) }}
@if ($database->isBackupSolutionAvailable() || $database->is_migrated) @@ -185,10 +186,6 @@ class="w-4 h-4 dark:text-warning text-coollabs"

Storages

Persistent storage to preserve data between deployments.
-
@foreach ($applications as $application) diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index d55bd801a..9e32cd22d 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -275,15 +275,9 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
Persistent storage to preserve data between deployments.
- @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 07/43] 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 08/43] 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 09/43] 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 11:12:19 +0100 Subject: [PATCH 10/43] 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 11/43] 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 12/43] 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 13/43] 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 14/43] 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 15/43] 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 16/43] 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 17/43] 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 18/43] 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 19/43] 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 20/43] 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 21/43] 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 22/43] 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 23/43] 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 34/43] 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 35/43] 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 36/43] 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 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 37/43] 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 38/43] 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 39/43] 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 40/43] 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 41/43] 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" /> + -