Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
github-actions[bot]
2026-10-04 19:31:07 +00:00
5 changed files with 129 additions and 69 deletions
+1 -3
View File
@@ -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 * * *',
+39 -37
View File
@@ -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;
+9 -2
View File
@@ -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 <body>: 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.');
}
@@ -223,39 +223,15 @@
</div>
@if (filled($dnsCopyText))
<div class="flex flex-wrap items-center justify-between gap-2"
x-data="{
copied: false,
async copyAll(text) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', '');
el.style.position = 'fixed';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
this.copied = true;
setTimeout(() => this.copied = false, 1000);
} catch (e) {
console.error('Copy failed', e);
}
}
}">
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="text-[12px] text-neutral-500 dark:text-fg-dim">
{{ count($dnsHints) }}
{{ count($dnsHints) === 1 ? 'entry' : 'entries' }}
· BIND zone format
</p>
<button type="button" class="button shrink-0"
title="Copy as BIND-compatible zone file"
@click.prevent="copyAll(@js($dnsCopyText))">
title="Copy as BIND-compatible zone file" x-data="copyButton"
@click.prevent="copy(@js($dnsCopyText))">
<span x-text="copied ? 'Copied' : 'Copy all'"></span>
</button>
</div>
@@ -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);
});