diff --git a/.ai/lessons.md b/.ai/lessons.md index 0c08f5d49..b4d51f943 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -5,3 +5,7 @@ ## Alpine x-transition + tw-animate-css exit animations flash at the end - Cause: `animate-out` keyframes default to `animation-fill-mode: none`. The element snaps back to its natural state when the keyframe ends. Alpine hides the element (display: none) only after its own timer (read from `transition-duration`), which starts ~2 rAF later than the animation. The gap shows the element at full opacity. - Rule: every `x-transition:leave` that uses tw-animate-css `animate-out` MUST also include `fill-mode-forwards`. - Rule: when a user reports UI flicker, check ALL layers of the animation stack (state reset timing, spinner flash, keyframe fill mode, focus restore) before you report the fix as complete. My first fix covered state reset and spinner only; the fill-mode snap was the visible one. + +## Displayed defaults must not become stored overrides +- When an edit form shows an inherited or computed default, trace an unchanged save and a related-field edit through persistence. +- Preserve the inherited state when the displayed value still equals the computed default; store an override only when the user selects a different value. diff --git a/.gitignore b/.gitignore index 9334baec4..efba1ec76 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ CHANGELOG.md /.workspaces /.superpowers/ /docs/superpowers/plans/ +/.ai/todo.md tests/Browser/Screenshots tests/v4/Browser/Screenshots ref diff --git a/app/Livewire/Project/Service/Domains.php b/app/Livewire/Project/Service/Domains.php index b4f76d12d..9774d20d3 100644 --- a/app/Livewire/Project/Service/Domains.php +++ b/app/Livewire/Project/Service/Domains.php @@ -1180,11 +1180,9 @@ public function startEdit(int $index): void $this->editingIndex = $index; $this->editingDomain = $this->domainRows[$index]['url']; $this->editingDomainParts = DomainUrlParts::split($this->editingDomain); - $app = $this->findServiceApp((int) $this->domainRows[$index]['service_application_id']); - $canonical = DomainPortOverrides::withoutPort($this->editingDomain); - $savedPort = ($app?->domain_port_overrides ?? [])[$canonical] ?? null; - if (filled($savedPort)) { - $this->editingDomainParts['port'] = (string) $savedPort; + $internalPort = $this->domainRows[$index]['internal_port'] ?? null; + if (filled($internalPort)) { + $this->editingDomainParts['port'] = (string) $internalPort; } $this->editingDomainPartsChanged = false; $this->editingServiceApplicationId = (int) $this->domainRows[$index]['service_application_id']; @@ -1219,8 +1217,16 @@ public function updateDomain(): void return; } - if ($this->editingDomainPartsChanged || filled($this->editingDomainParts['host'] ?? null)) { - $this->editingDomain = DomainUrlParts::compose(...$this->editingDomainParts); + $editingDomainParts = $this->editingDomainParts; + $editingRow = $this->domainRows[$this->editingIndex]; + if ( + ! ($editingRow['has_port_override'] ?? false) + && (string) ($editingDomainParts['port'] ?? '') === (string) ($editingRow['internal_port'] ?? '') + ) { + $editingDomainParts['port'] = ''; + } + if ($this->editingDomainPartsChanged || filled($editingDomainParts['host'] ?? null)) { + $this->editingDomain = DomainUrlParts::compose(...$editingDomainParts); } $this->validateOnly('editingDomain'); diff --git a/tests/Feature/ServiceDomainsTest.php b/tests/Feature/ServiceDomainsTest.php index da6701656..ef61912f0 100644 --- a/tests/Feature/ServiceDomainsTest.php +++ b/tests/Feature/ServiceDomainsTest.php @@ -907,9 +907,18 @@ Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])]) ->assertSet('domainRows.0.internal_port', 3000) ->assertSet('domainRows.0.has_port_override', false) + ->set('dnsValidationEnabled', false) + ->call('startEdit', 0) + ->assertSet('editingDomainParts.port', '3000') + ->call('updateDomain') + ->assertHasNoErrors() ->assertSee('Internal port 3000') ->assertSee('Inherited from the Coolify service port', false) ->assertDontSee('No internal port'); + + expect($this->apiApp->fresh()) + ->fqdn->toBe('https://api.example.com') + ->domain_port_overrides->toBeNull(); }); it('shows a custom internal port badge for a service domain override', function () {