From 981fc127b5054c19ae3d71897e49d08f53cc6154 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:51:21 +0100 Subject: [PATCH 1/4] fix: move base directory path normalization to frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change wire:model.blur to wire:model.defer to prevent backend requests during form navigation. Add Alpine.js path normalization functions that run on blur, fixing tab focus issues while keeping path validation purely on the frontend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Project/New/GithubPrivateRepository.php | 10 ------ .../Project/New/PublicGitRepository.php | 20 ------------ ...ub-private-repository-deploy-key.blade.php | 31 ++++++++++++++++--- .../new/github-private-repository.blade.php | 30 +++++++++++++++--- .../new/public-git-repository.blade.php | 31 ++++++++++++++++--- 5 files changed, 77 insertions(+), 45 deletions(-) diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 27ecacb99..5dd508c29 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -75,16 +75,6 @@ public function mount() $this->github_apps = GithubApp::private(); } - public function updatedBaseDirectory() - { - if ($this->base_directory) { - $this->base_directory = rtrim($this->base_directory, '/'); - if (! str($this->base_directory)->startsWith('/')) { - $this->base_directory = '/'.$this->base_directory; - } - } - } - public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 89814ee7f..2fffff6b9 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -107,26 +107,6 @@ public function mount() $this->query = request()->query(); } - public function updatedBaseDirectory() - { - if ($this->base_directory) { - $this->base_directory = rtrim($this->base_directory, '/'); - if (! str($this->base_directory)->startsWith('/')) { - $this->base_directory = '/'.$this->base_directory; - } - } - } - - public function updatedDockerComposeLocation() - { - if ($this->docker_compose_location) { - $this->docker_compose_location = rtrim($this->docker_compose_location, '/'); - if (! str($this->docker_compose_location)->startsWith('/')) { - $this->docker_compose_location = '/'.$this->docker_compose_location; - } - } - } - public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { diff --git a/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php b/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php index 6d644ba2c..596559817 100644 --- a/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php +++ b/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php @@ -61,12 +61,33 @@ class="loading loading-xs dark:text-warning loading-spinner"> @endif @if ($build_pack === 'dockercompose') -
- - + + + x-model="composeLocation" @blur="normalizeComposeLocation()" />
Compose file location in your repository: @if ($build_pack === 'dockercompose') -
- + + x-model="baseDir" @blur="normalizeBaseDir()" /> + x-model="composeLocation" @blur="normalizeComposeLocation()" />
Compose file location in your repository: @if ($build_pack === 'dockercompose') -
- - + + + x-model="composeLocation" @blur="normalizeComposeLocation()" />
Compose file location in your repository: Date: Tue, 2 Dec 2025 13:37:41 +0100 Subject: [PATCH 2/4] fix: apply frontend path normalization to general settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same frontend path normalization pattern from commit f6398f7cf to the General Settings page for consistency across all forms. Changes: - Add Alpine.js path normalization to Docker Compose section (base directory + compose location) - Add Alpine.js path normalization to non-Docker Compose section (base directory + dockerfile location) - Change wire:model to wire:model.defer to prevent backend requests during tab navigation - Add @blur event handlers for immediate path normalization feedback - Backend normalization remains as defensive fallback This ensures consistent validation behavior and fixes potential tab focus issues on the General Settings page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Application/General.php | 31 +++++----- .../project/application/general.blade.php | 59 +++++++++++++++---- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index ef474fb02..56d45ae19 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -606,13 +606,6 @@ public function generateDomain(string $serviceName) } } - public function updatedBaseDirectory() - { - if ($this->buildPack === 'dockercompose') { - $this->loadComposeFile(); - } - } - public function updatedIsStatic($value) { if ($value) { @@ -791,6 +784,7 @@ public function submit($showToaster = true) $oldPortsExposes = $this->application->ports_exposes; $oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; $oldDockerComposeLocation = $this->initialDockerComposeLocation; + $oldBaseDirectory = $this->application->base_directory; // Process FQDN with intermediate variable to avoid Collection/string confusion $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); @@ -821,6 +815,16 @@ public function submit($showToaster = true) return; // Stop if there are conflicts and user hasn't confirmed } + // Normalize paths BEFORE validation + if ($this->baseDirectory && $this->baseDirectory !== '/') { + $this->baseDirectory = rtrim($this->baseDirectory, '/'); + $this->application->base_directory = $this->baseDirectory; + } + if ($this->publishDirectory && $this->publishDirectory !== '/') { + $this->publishDirectory = rtrim($this->publishDirectory, '/'); + $this->application->publish_directory = $this->publishDirectory; + } + $this->application->save(); if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); @@ -828,7 +832,10 @@ public function submit($showToaster = true) $this->application->save(); } - if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) { + // Validate docker compose file path when base directory OR compose location changes + if ($this->buildPack === 'dockercompose' && + ($oldDockerComposeLocation !== $this->dockerComposeLocation || + $oldBaseDirectory !== $this->baseDirectory)) { $compose_return = $this->loadComposeFile(showToast: false); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { return; @@ -855,14 +862,6 @@ public function submit($showToaster = true) $this->application->ports_exposes = $port; } } - if ($this->baseDirectory && $this->baseDirectory !== '/') { - $this->baseDirectory = rtrim($this->baseDirectory, '/'); - $this->application->base_directory = $this->baseDirectory; - } - if ($this->publishDirectory && $this->publishDirectory !== '/') { - $this->publishDirectory = rtrim($this->publishDirectory, '/'); - $this->application->publish_directory = $this->publishDirectory; - } if ($this->buildPack === 'dockercompose') { $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); if ($this->application->isDirty('docker_compose_domains')) { diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index d1a331d1a..8cf46d2f3 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -241,12 +241,32 @@ @else
@endcan -
- +
+ + wire:model.defer="dockerComposeLocation" label="Docker Compose Location" + helper="It is calculated together with the Base Directory:
{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}" + x-model="composeLocation" @blur="normalizeComposeLocation()" />
@else -
- +
+ @if ($application->build_pack === 'dockerfile' && !$application->dockerfile) - + x-bind:disabled="!canUpdate" x-model="dockerfileLocation" @blur="normalizeDockerfileLocation()" /> @endif @if ($application->build_pack === 'dockerfile') From 1499135409818334b18002af916d8b12babce712 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:29:52 +0100 Subject: [PATCH 3/4] fix: prevent invalid paths from being saved to database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move compose file validation BEFORE database save to prevent invalid base directory and docker compose location values from being persisted when validation fails. Changes: - Move compose file validation before $this->application->save() - Restore original values when validation fails - Add resetErrorBag() to clear stale validation errors This fixes two bugs: 1. Invalid paths were saved to DB even when validation failed 2. Error messages persisted after correcting to valid path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Application/General.php | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 56d45ae19..46a459fe2 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -779,6 +779,7 @@ public function submit($showToaster = true) try { $this->authorize('update', $this->application); + $this->resetErrorBag(); $this->validate(); $oldPortsExposes = $this->application->ports_exposes; @@ -825,23 +826,30 @@ public function submit($showToaster = true) $this->application->publish_directory = $this->publishDirectory; } - $this->application->save(); - if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { - $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->application->custom_labels = base64_encode($this->customLabels); - $this->application->save(); - } - - // Validate docker compose file path when base directory OR compose location changes + // Validate docker compose file path BEFORE saving to database + // This prevents invalid paths from being persisted when validation fails if ($this->buildPack === 'dockercompose' && ($oldDockerComposeLocation !== $this->dockerComposeLocation || $oldBaseDirectory !== $this->baseDirectory)) { $compose_return = $this->loadComposeFile(showToast: false); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { + // Restore original values - don't persist invalid data + $this->baseDirectory = $oldBaseDirectory; + $this->dockerComposeLocation = $oldDockerComposeLocation; + $this->application->base_directory = $oldBaseDirectory; + $this->application->docker_compose_location = $oldDockerComposeLocation; + return; } } + $this->application->save(); + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + } + if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) { $this->resetDefaultLabels(); } From dae680317385f2a495b0ae2b1687d2ce8f555256 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:57:15 +0100 Subject: [PATCH 4/4] fix: restore original base_directory on compose validation failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Application::loadComposeFile method's finally block always saves the model, which was persisting invalid base_directory values when validation failed. Changes: - Add restoreBaseDirectory and restoreDockerComposeLocation parameters to loadComposeFile() in both Application model and General component - The finally block now restores BOTH base_directory and docker_compose_location to the provided original values before saving - When called from submit(), pass the original DB values so they are restored on failure instead of the new invalid values This ensures invalid paths are never persisted to the database. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Application/General.php | 21 ++++++++++++++------ app/Models/Application.php | 7 +++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 46a459fe2..c84de9d8d 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -521,7 +521,7 @@ public function instantSave() } } - public function loadComposeFile($isInit = false, $showToast = true) + public function loadComposeFile($isInit = false, $showToast = true, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null) { try { $this->authorize('update', $this->application); @@ -530,7 +530,7 @@ public function loadComposeFile($isInit = false, $showToast = true) return; } - ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); + ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit, $restoreBaseDirectory, $restoreDockerComposeLocation); if (is_null($this->parsedServices)) { $showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); @@ -831,13 +831,22 @@ public function submit($showToaster = true) if ($this->buildPack === 'dockercompose' && ($oldDockerComposeLocation !== $this->dockerComposeLocation || $oldBaseDirectory !== $this->baseDirectory)) { - $compose_return = $this->loadComposeFile(showToast: false); + // Pass original values to loadComposeFile so it can restore them on failure + // The finally block in Application::loadComposeFile will save these original + // values if validation fails, preventing invalid paths from being persisted + $compose_return = $this->loadComposeFile( + isInit: false, + showToast: false, + restoreBaseDirectory: $oldBaseDirectory, + restoreDockerComposeLocation: $oldDockerComposeLocation + ); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { - // Restore original values - don't persist invalid data + // Validation failed - restore original values to component properties $this->baseDirectory = $oldBaseDirectory; $this->dockerComposeLocation = $oldDockerComposeLocation; - $this->application->base_directory = $oldBaseDirectory; - $this->application->docker_compose_location = $oldDockerComposeLocation; + // The model was saved by loadComposeFile's finally block with original values + // Refresh to sync component with database state + $this->application->refresh(); return; } diff --git a/app/Models/Application.php b/app/Models/Application.php index 6e920f8e6..7bddce32b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1511,9 +1511,11 @@ public function parse(int $pull_request_id = 0, ?int $preview_id = null) } } - public function loadComposeFile($isInit = false) + public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null) { - $initialDockerComposeLocation = $this->docker_compose_location; + // Use provided restore values or capture current values as fallback + $initialDockerComposeLocation = $restoreDockerComposeLocation ?? $this->docker_compose_location; + $initialBaseDirectory = $restoreBaseDirectory ?? $this->base_directory; if ($isInit && $this->docker_compose_raw) { return; } @@ -1580,6 +1582,7 @@ public function loadComposeFile($isInit = false) throw new \RuntimeException($e->getMessage()); } finally { $this->docker_compose_location = $initialDockerComposeLocation; + $this->base_directory = $initialBaseDirectory; $this->save(); $commands = collect([ "rm -rf /tmp/{$uuid}",