fix(livewire): preserve wire:dirty across DB status broadcasts

The earlier refreshStatus fix kept user-typed values intact but Livewire still
absorbed deferred wire:model values into the snapshot on every broadcast-
triggered roundtrip, clearing the unsaved-changes indicator and making the form
look auto-saved. Move all status-derived display (DB URLs, SSL toggle/mode,
cert expiry) out of each DB General form into a sibling StatusInfo Livewire
component, so the form never roundtrips on broadcasts.

Shared scaffolding lives in App\Traits\HasDatabaseStatusInfo plus an x-database-
status-info Blade component, leaving each per-DB StatusInfo class as a ~20-50
line declaration of label, SSL mode options, and SSL save hooks. Parents
dispatch databaseUpdated from save methods so the sibling refreshes after writes.

Tests cover the architecture (no DB form subscribes to status broadcasts) and
the sibling's refresh-on-status-change behavior.
This commit is contained in:
Aditya Tripathi committed 2026-05-21 08:31:08 +00:00
1 parent 9aee01d5a0
commit e7e65831a7
27 files changed
+603 -1308

No files matched your search

+4 -102
View File
@@ -4,11 +4,9 @@ namespace App\Livewire\Project\Database\Keydb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\StandaloneKeydb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
@@ -42,39 +40,17 @@ class General extends Component
public ?string $customDockerRunOptions = null;
public ?string $dbUrl = null;
public ?string $dbUrlPublic = null;
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
public bool $enable_ssl = false;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
public function mount()
{
try {
@@ -86,12 +62,6 @@ class General extends Component
return;
}
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -99,7 +69,7 @@ class General extends Component
protected function rules(): array
{
$baseRules = [
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'keydbConf' => 'nullable|string',
@@ -112,13 +82,8 @@ class General extends Component
'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
'dbUrlPublic' => 'nullable|string',
'isLogDrainEnabled' => 'nullable|boolean',
'enable_ssl' => 'boolean',
];
return $baseRules;
}
protected function messages(): array
@@ -154,11 +119,7 @@ class General extends Component
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
} else {
$this->name = $this->database->name;
$this->description = $this->database->description;
@@ -171,9 +132,6 @@ class General extends Component
$this->publicPortTimeout = $this->database->public_port_timeout;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
}
@@ -222,6 +180,7 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->dispatch('databaseUpdated');
} catch (\Throwable $e) {
$this->isPublic = ! $this->isPublic;
$this->syncData(true);
@@ -233,6 +192,7 @@ class General extends Component
public function databaseProxyStopped()
{
$this->syncData();
$this->dispatch('databaseUpdated');
}
public function submit()
@@ -248,6 +208,7 @@ class General extends Component
}
$this->syncData(true);
$this->dispatch('success', 'Database updated.');
$this->dispatch('databaseUpdated');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
@@ -259,65 +220,6 @@ class General extends Component
}
}
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = $this->server->sslCertificates()
->where('is_ca_certificate', true)
->first();
if (! $caCert) {
$this->server->generateCaCertificate();
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();