From 968ea69e7df3a166df590e148faa9f9c9eb832d0 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 7 Aug 2026 18:05:51 +0200
Subject: [PATCH 1/6] fix(ui): show loading state on compose validate button
Disable the Validate control and show a spinner until validateCompose
finishes, then clear state via compose-validate-finished.
---
.../livewire/project/service/edit-compose.blade.php | 2 +-
.../views/livewire/project/service/stack-form.blade.php | 9 +++++++--
tests/Feature/ComposeEditorLayoutTest.php | 5 +++++
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/resources/views/livewire/project/service/edit-compose.blade.php b/resources/views/livewire/project/service/edit-compose.blade.php
index 3acbfcc2a..78400cb76 100644
--- a/resources/views/livewire/project/service/edit-compose.blade.php
+++ b/resources/views/livewire/project/service/edit-compose.blade.php
@@ -1,6 +1,6 @@
$dispatch('compose-validate-finished'))"
@compose-save.window="$wire.saveEditedCompose()"
class="flex min-h-0 flex-col gap-3">
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php
index 65eb0e3fb..89dfa0e5c 100644
--- a/resources/views/livewire/project/service/stack-form.blade.php
+++ b/resources/views/livewire/project/service/stack-form.blade.php
@@ -12,7 +12,8 @@
-
@@ -20,7 +21,11 @@
@if (blank($service->service_type))
-
Validate
+
+
+ Validate
+
@endif
diff --git a/tests/Feature/ComposeEditorLayoutTest.php b/tests/Feature/ComposeEditorLayoutTest.php
index 953bcc0e0..d8538e82d 100644
--- a/tests/Feature/ComposeEditorLayoutTest.php
+++ b/tests/Feature/ComposeEditorLayoutTest.php
@@ -13,6 +13,10 @@ it('uses the large modal treatment for the compose editor', function () {
->toContain("\$dispatch('compose-preview-toggle')")
->toContain("\$dispatch('compose-save')")
->toContain('@compose-save-finished.window="saving = false"')
+ ->toContain('@compose-validate-finished.window="validating = false"')
+ ->toContain('@click="validating = true; $dispatch(\'compose-validate\')"')
+ ->toContain('x-bind:disabled="validating"')
+ ->toContain('')
->toContain('')
->toContain('x-bind:disabled="saving"')
->not->toContain('name="refresh"')
@@ -36,6 +40,7 @@ it('renders the compose editor with clear guidance settings and actions', functi
->toContain('min-h-[24rem]')
->toContain('@compose-preview-toggle.window')
->toContain('@compose-save.window')
+ ->toContain('@compose-validate.window="$wire.validateCompose().finally(() => $dispatch(\'compose-validate-finished\'))"')
->not->toContain("finally(() => \$dispatch('compose-save-finished'))")
->not->toContain('sticky bottom-0')
->not->toContain('Cancel')
From 47da47d8e03c019e587cb2e568b74e008c365690 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 7 Aug 2026 18:18:58 +0200
Subject: [PATCH 2/6] fix(docker): escape Traefik www redirect replacements for
Compose
Double-escape ${n} in Traefik redirectregex.replacement labels so Docker
Compose emits $${n} and Traefik still receives the correct capture groups.
---
bootstrap/helpers/docker.php | 4 ++--
tests/Feature/ApplicationParserDockerComposeDomainsTest.php | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 52783ff4d..b688ffbb8 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -555,12 +555,12 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$to_non_www_name = "{$loop}-{$uuid}-to-non-www";
$redirect_to_non_www = [
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)",
- "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\${1}://\${2}",
+ "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\$\${1}://\$\${2}",
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false",
];
$redirect_to_www = [
"traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)",
- "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\${1}://www.\${2}",
+ "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\$\${1}://www.\$\${2}",
"traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false",
];
if ($schema === 'https') {
diff --git a/tests/Feature/ApplicationParserDockerComposeDomainsTest.php b/tests/Feature/ApplicationParserDockerComposeDomainsTest.php
index eca364238..539dba5f2 100644
--- a/tests/Feature/ApplicationParserDockerComposeDomainsTest.php
+++ b/tests/Feature/ApplicationParserDockerComposeDomainsTest.php
@@ -301,7 +301,7 @@ YAML,
$parsedCompose = applicationParser($application);
$labels = collect(data_get($parsedCompose, 'services.frontend.labels'));
- expect($labels->contains(fn (string $label): bool => str_contains($label, 'redirectregex.replacement=${1}://www.${2}')))->toBeTrue();
+ expect($labels->contains(fn (string $label): bool => str_contains($label, 'redirectregex.replacement=$${1}://www.$${2}')))->toBeTrue();
});
test('compose domain reconciliation preserves stored domains when parsing returns no services', function () {
From e50108a90588f5704cdda39f1e41e3d9430f5277 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 7 Aug 2026 18:31:19 +0200
Subject: [PATCH 3/6] fix(ui): prefer Compose env values and polish error pages
When a managed variable shares a Compose-defined key, show the
hardcoded Compose value as read-only and exclude it from managed
rows. Render Contact support as a button, use the collapsible
component for 419 proxy help, and style its list/code markers.
---
.../Shared/EnvironmentVariable/All.php | 38 +++++++++++++------
resources/css/app.css | 6 ++-
.../views/components/error-page.blade.php | 7 ++--
resources/views/errors/419.blade.php | 6 +--
.../project/application/general.blade.php | 2 -
.../project/application/heading.blade.php | 2 -
.../Feature/EnvironmentVariableSearchTest.php | 28 ++++++++++++++
tests/Feature/ErrorPagesRedesignTest.php | 13 +++++++
8 files changed, 78 insertions(+), 24 deletions(-)
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 4c4907200..89130799a 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -512,6 +512,11 @@ class All extends Component
->where('resourceable_id', $this->resource->id)
->where('is_preview', $isPreview);
+ $hardcodedKeys = $this->hardcodedEnvironmentVariableKeys();
+ if ($hardcodedKeys !== []) {
+ $query->whereNotIn('key', $hardcodedKeys);
+ }
+
if ($this->serviceFilters !== []) {
$query->whereRaw('1 = 0');
}
@@ -716,18 +721,6 @@ class All extends Component
return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
});
- // Filter out variables that exist in database (user has overridden/managed them)
- // For preview, check against preview variables; for production, check against production variables
- if ($isPreview) {
- $managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
- } else {
- $managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
- }
-
- $hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
- return ! in_array($var['key'], $managedKeys);
- });
-
if ($this->searchTerm() !== '') {
$hardcodedVars = $hardcodedVars->filter(function ($var) {
return str($var['key'])->contains($this->searchTerm(), true);
@@ -749,6 +742,27 @@ class All extends Component
return $hardcodedVars;
}
+ /** @return list */
+ private function hardcodedEnvironmentVariableKeys(): array
+ {
+ if (! $this->showsHardcodedEnvironmentVariables()) {
+ return [];
+ }
+
+ $dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
+
+ if (blank($dockerComposeRaw)) {
+ return [];
+ }
+
+ return extractHardcodedEnvironmentVariables($dockerComposeRaw)
+ ->pluck('key')
+ ->reject(fn (string $key): bool => str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']))
+ ->unique()
+ ->values()
+ ->all();
+ }
+
public function getDevView()
{
$this->variables = $this->formatEnvironmentVariables($this->getEnvironmentVariables(false, false));
diff --git a/resources/css/app.css b/resources/css/app.css
index 98e7d3899..bcbabe389 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -1207,7 +1207,8 @@ html[data-theme="custom"] textarea:disabled {
color: var(--color-accent);
}
-.error-extra details ul {
+.error-extra details ul,
+.error-proxy-help ul {
margin: 0.5rem 0 0;
padding-left: 1.125rem;
display: flex;
@@ -1215,7 +1216,8 @@ html[data-theme="custom"] textarea:disabled {
gap: 0.375rem;
}
-.error-extra details code {
+.error-extra details code,
+.error-proxy-help code {
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 0.05rem 0.3rem;
diff --git a/resources/views/components/error-page.blade.php b/resources/views/components/error-page.blade.php
index 7a29eb0c3..11ca60cc8 100644
--- a/resources/views/components/error-page.blade.php
+++ b/resources/views/components/error-page.blade.php
@@ -55,10 +55,11 @@
- Contact support
-
+
+ Contact support
+
+
@endif
diff --git a/resources/views/errors/419.blade.php b/resources/views/errors/419.blade.php
index c04efcf59..585e887a5 100644
--- a/resources/views/errors/419.blade.php
+++ b/resources/views/errors/419.blade.php
@@ -11,14 +11,14 @@
:show-dashboard="false"
primary-href="/login"
primary-label="Back to login">
-
- Using a reverse proxy or Cloudflare Tunnel?
+
- Set your domain in Settings → FQDN to match the URL you use to access Coolify.
- Cloudflare users: disable Browser Integrity Check and Under Attack Mode for your Coolify domain, as these can interrupt login sessions.
- If you can still access Coolify via
localhost, log in there first to configure your FQDN.
-
+
+ @livewireScripts