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.
This commit is contained in:
Andras Bacsai 2026-08-07 18:31:19 +02:00
parent 47da47d8e0
commit e50108a905
8 changed files with 78 additions and 24 deletions

View file

@ -512,6 +512,11 @@ private function managedEnvironmentVariablesQuery(bool $isPreview): Builder
->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 @@ protected function getHardcodedVariables(bool $isPreview)
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 @@ protected function getHardcodedVariables(bool $isPreview)
return $hardcodedVars;
}
/** @return list<string> */
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));

View file

@ -1207,7 +1207,8 @@ .error-extra details summary:hover {
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 @@ .error-extra details ul {
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;

View file

@ -55,10 +55,11 @@
<a
target="_blank"
rel="noopener noreferrer"
class="error-contact-link"
href="{{ config('constants.urls.contact') }}">
<x-forms.button type="button">
Contact support
<x-external-link class="inline-flex size-3 text-current" />
</x-forms.button>
</a>
@endif
</div>

View file

@ -11,14 +11,14 @@
:show-dashboard="false"
primary-href="/login"
primary-label="Back to login">
<details>
<summary>Using a reverse proxy or Cloudflare Tunnel?</summary>
<x-forms.collapsible title="Using a reverse proxy or Cloudflare Tunnel?" class="error-proxy-help">
<ul>
<li>Set your domain in <strong>Settings &rarr; FQDN</strong> to match the URL you use to access Coolify.</li>
<li>Cloudflare users: disable <strong>Browser Integrity Check</strong> and <strong>Under Attack Mode</strong> for your Coolify domain, as these can interrupt login sessions.</li>
<li>If you can still access Coolify via <code>localhost</code>, log in there first to configure your FQDN.</li>
</ul>
</details>
</x-forms.collapsible>
</x-error-page>
@livewireScripts
</body>
@endsection

View file

@ -7,8 +7,6 @@
}">
<form wire:submit='submit' class="application-settings-form flex flex-col">
<x-unsaved-bar action="submit" />
{{-- Temporarily hidden: the "Compose parser" dev hint and the "View details"
resource-details modal trigger. --}}
<div class="application-settings-grid flex flex-col gap-6">
<x-application.settings-section id="application-details-section" title="Application details" helper="Name the application and choose the build strategy Coolify should use to deploy it." class="application-details-card">
@if ($buildPack === 'dockercompose')

View file

@ -180,8 +180,6 @@ class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="me
<div
class="resource-heading-navbar application-heading-actions flex w-full min-w-0 items-center justify-start gap-1 overflow-visible xl:w-auto xl:justify-end">
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5">
{{-- Status badge temporarily hidden will be redesigned later:
<x-status.index :resource="$application" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" /> --}}
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<span class="px-2 text-[13px] text-neutral-500 dark:text-fg-dim">Load a Compose file to deploy.</span>
@else

View file

@ -182,6 +182,34 @@
->toBe(['API_TOKEN']);
});
it('shows a Compose-defined value as read-only when a managed variable has the same key', function () {
$service = Service::factory()->create([
'environment_id' => $this->environment->id,
'docker_compose_raw' => <<<'YAML'
services:
app:
image: nginx
environment:
- API_TOKEN=from-compose
YAML,
]);
EnvironmentVariable::create([
'key' => 'API_TOKEN',
'value' => 'from-environment-tab',
'resourceable_type' => Service::class,
'resourceable_id' => $service->id,
]);
$component = Livewire::test(All::class, ['resource' => $service])
->call('loadEnvironmentVariables');
expect($component->instance()->environmentVariablePageRows)
->toHaveCount(1)
->and($component->instance()->environmentVariablePageRows->first()['kind'])->toBe('hardcoded')
->and($component->instance()->environmentVariablePageRows->first()['environmentVariable']['value'])->toBe('from-compose');
});
it('searches service environment variables without requiring preview variables', function () {
$service = Service::factory()->create([
'environment_id' => $this->environment->id,

View file

@ -57,9 +57,22 @@
->toContain('/login')
->toContain('Back to login')
->toContain('Using a reverse proxy or Cloudflare Tunnel?')
->toContain('x-data="{ open: false }"')
->toContain('x-on:click="open = !open"')
->toContain('livewire.js')
->not->toContain('>Dashboard</');
});
it('renders contact support as a button', function () {
$exception = new HttpException(404, 'Not found');
$html = view('errors.404', ['exception' => $exception])->render();
expect($html)
->toContain('href="'.config('constants.urls.contact').'"')
->toMatch('/<button[^>]*>\s*Contact support/s');
});
it('shows purified exception message on 500 page', function () {
$exception = new RuntimeException('Database connection failed');