From cfe9e0eee0567428694a51e1590b0f6245a0663c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:33:22 +0200 Subject: [PATCH 1/2] fix(settings): load instance backup page without a backup schedule /settings/backup returned 500 when the coolify-db resource existed without a scheduled backup. Configure backup now reuses the existing coolify-db and creates only the missing schedule for the root team, without forcing id 0. Closes #12074 --- app/Console/Commands/Init.php | 4 +- app/Livewire/SettingsBackup.php | 76 +++++++++--------- .../Feature/SettingsBackupEmptyStateTest.php | 77 +++++++++++++++++++ 3 files changed, 117 insertions(+), 40 deletions(-) diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index f087002da1..e21a2f1c81 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -257,10 +257,8 @@ class Init extends Command $database = StandalonePostgresql::withTrashed()->find(0); if ($database && $database->trashed()) { $database->restore(); - $scheduledBackup = ScheduledDatabaseBackup::find(0); - if (! $scheduledBackup) { + if (! $database->scheduledBackups()->exists()) { ScheduledDatabaseBackup::create([ - 'id' => 0, 'enabled' => true, 'save_s3' => false, 'frequency' => '0 0 * * *', diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index a498f9169b..43563e7c83 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -71,7 +71,7 @@ class SettingsBackup extends Component $this->backup->enabled = false; $this->backup->save(); } - $this->executions = $this->backup->executions; + $this->executions = $this->backup?->executions ?? []; } $this->settings = $settings; $this->s3s = $s3s; @@ -81,44 +81,46 @@ class SettingsBackup extends Component { try { $this->authorize('update', $this->settings); - $server = Server::findOrFail(0); - $out = instant_remote_process(['docker inspect coolify-db'], $server); - $envs = format_docker_envs_to_json($out); - $postgres_password = $envs['POSTGRES_PASSWORD']; - $postgres_user = $envs['POSTGRES_USER']; - $postgres_db = $envs['POSTGRES_DB']; - $this->database = new StandalonePostgresql; - $this->database->forceFill([ - 'id' => 0, - 'name' => 'coolify-db', - 'description' => 'Coolify database', - 'postgres_user' => $postgres_user, - 'postgres_password' => $postgres_password, - 'postgres_db' => $postgres_db, - 'status' => 'running', - 'destination_type' => StandaloneDocker::class, - 'destination_id' => 0, - ]); - $this->database->save(); - $this->backup = ScheduledDatabaseBackup::create([ - 'id' => 0, - 'enabled' => true, - 'save_s3' => false, - 'frequency' => '0 0 * * *', - 'database_id' => $this->database->id, - 'database_type' => StandalonePostgresql::class, - 'team_id' => currentTeam()->id, - ]); + $this->database = StandalonePostgresql::whereName('coolify-db')->first(); + if (! $this->database) { + $server = Server::findOrFail(0); + $out = instant_remote_process(['docker inspect coolify-db'], $server); + $envs = format_docker_envs_to_json($out); + $this->database = new StandalonePostgresql; + $this->database->forceFill([ + 'id' => 0, + 'name' => 'coolify-db', + 'description' => 'Coolify database', + 'postgres_user' => $envs['POSTGRES_USER'], + 'postgres_password' => $envs['POSTGRES_PASSWORD'], + 'postgres_db' => $envs['POSTGRES_DB'], + 'status' => 'running', + 'destination_type' => StandaloneDocker::class, + 'destination_id' => 0, + ]); + $this->database->save(); + } + $this->backup = $this->database->scheduledBackups()->first() + ?? ScheduledDatabaseBackup::create([ + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 0 * * *', + 'database_id' => $this->database->id, + 'database_type' => StandalonePostgresql::class, + 'team_id' => 0, + ]); $this->database->refresh(); $this->backup->refresh(); - auditLog('ui.database.backup_schedule_created', [ - 'team_id' => $this->backup->team_id, - 'database_uuid' => $this->database->uuid, - 'database_name' => $this->database->name, - 'backup_uuid' => $this->backup->uuid, - 'frequency' => $this->backup->frequency, - 'save_s3' => (bool) $this->backup->save_s3, - ]); + if ($this->backup->wasRecentlyCreated) { + auditLog('ui.database.backup_schedule_created', [ + 'team_id' => $this->backup->team_id, + 'database_uuid' => $this->database->uuid, + 'database_name' => $this->database->name, + 'backup_uuid' => $this->backup->uuid, + 'frequency' => $this->backup->frequency, + 'save_s3' => (bool) $this->backup->save_s3, + ]); + } $this->s3s = S3Storage::whereTeamId(0)->get(); $this->uuid = $this->database->uuid; diff --git a/tests/Feature/SettingsBackupEmptyStateTest.php b/tests/Feature/SettingsBackupEmptyStateTest.php index 3811c850e3..7bdf94341e 100644 --- a/tests/Feature/SettingsBackupEmptyStateTest.php +++ b/tests/Feature/SettingsBackupEmptyStateTest.php @@ -2,11 +2,15 @@ use App\Livewire\SettingsBackup; use App\Models\InstanceSettings; +use App\Models\ScheduledDatabaseBackup; use App\Models\Server; +use App\Models\StandaloneDocker; +use App\Models\StandalonePostgresql; use App\Models\Team; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Once; use Livewire\Livewire; @@ -84,3 +88,76 @@ test('instance backup settings show a configure button when backup is not set up ->assertSee('Configure backup') ->assertSeeHtml('wire:click="addCoolifyDatabase"'); }); + +function settingsBackupInstanceDatabaseWithoutSchedule(): StandalonePostgresql +{ + Server::flushIdentityMap(); + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $server = Server::factory()->create([ + 'id' => 0, + 'team_id' => $rootTeam->id, + 'ip' => '127.0.0.1', + ]); + $server->settings()->update([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ]); + StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail()->forceFill(['id' => 0])->save(); + InstanceSettings::forceCreate(['id' => 0]); + Once::flush(); + + $database = new StandalonePostgresql; + $database->forceFill([ + 'id' => 0, + 'name' => 'coolify-db', + 'description' => 'Coolify database', + 'postgres_user' => 'coolify', + 'postgres_password' => 'password', + 'postgres_db' => 'coolify', + 'status' => 'running', + 'destination_type' => StandaloneDocker::class, + 'destination_id' => 0, + ]); + $database->save(); + + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + test()->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + return $database; +} + +test('instance backup settings render when the instance database has no backup schedule', function () { + settingsBackupInstanceDatabaseWithoutSchedule(); + + Livewire::test(SettingsBackup::class) + ->assertOk() + ->assertSet('executions', []) + ->assertSee('Backup is not configured') + ->assertSee('Configure backup'); +}); + +test('configure backup creates only the missing schedule for an existing instance database', function () { + $database = settingsBackupInstanceDatabaseWithoutSchedule(); + $otherTeam = Team::factory()->create(); + auth()->user()->teams()->attach($otherTeam->id, ['role' => 'owner']); + session(['currentTeam' => ['id' => $otherTeam->id]]); + Process::fake(); + + Livewire::test(SettingsBackup::class) + ->call('addCoolifyDatabase') + ->assertHasNoErrors() + ->assertSee('Instance database'); + + Process::assertNothingRan(); + $backup = $database->scheduledBackups()->sole(); + + expect(StandalonePostgresql::query()->where('name', 'coolify-db')->count())->toBe(1) + ->and($backup->id)->not->toBe(0) + ->and($backup->team_id)->toBe(0) + ->and($backup->enabled)->toBeTruthy() + ->and($backup->frequency)->toBe('0 0 * * *') + ->and(ScheduledDatabaseBackup::query()->count())->toBe(1); +}); From 34e4da1095726c6f9e4bb4272aa01b5a6dd43901 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:36:49 +0200 Subject: [PATCH 2/2] fix(ui): copy DNS record values on plain http instances On non-secure origins the copy button falls back to execCommand('copy') with a temporary textarea. The textarea was appended to , outside the x-trap focus trap of the DNS entries modal, so the trap pulled focus back and the selection was lost before the copy. The button showed success, but nothing was copied. Append the textarea next to the button, select it explicitly, and restore focus afterwards. "Copy all" now uses the shared copyButton handler instead of its own duplicate fallback that ignored failures. Closes #12075 --- resources/js/copy-button.js | 11 +++++-- .../shared/cloudflare-autoconfigure.blade.php | 30 ++----------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/resources/js/copy-button.js b/resources/js/copy-button.js index 0ce8d5d67d..b62eaa120e 100644 --- a/resources/js/copy-button.js +++ b/resources/js/copy-button.js @@ -12,15 +12,22 @@ export function initializeCopyButtonComponent() { await navigator.clipboard.writeText(value); } else { // Deprecated, but the only copy path on plain http (non-secure contexts). + // The textarea goes next to the button, not into : a modal's x-trap + // pulls focus back from nodes outside it, which clears the selection. + const previousFocus = document.activeElement; const textarea = document.createElement('textarea'); textarea.value = value; textarea.setAttribute('readonly', ''); + textarea.setAttribute('tabindex', '-1'); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; - document.body.appendChild(textarea); + this.$el.after(textarea); + textarea.focus({ preventScroll: true }); textarea.select(); + textarea.setSelectionRange(0, textarea.value.length); const ok = document.execCommand('copy'); - document.body.removeChild(textarea); + textarea.remove(); + previousFocus?.focus?.({ preventScroll: true }); if (!ok) { throw new Error('Copy command was rejected.'); } diff --git a/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php b/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php index 8d102ff47f..9b565c4e98 100644 --- a/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php +++ b/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php @@ -223,39 +223,15 @@ @if (filled($dnsCopyText)) -
+

{{ count($dnsHints) }} {{ count($dnsHints) === 1 ? 'entry' : 'entries' }} ยท BIND zone format