From 602207351979d031a42b9eec4eda30eadc650c9f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:05:19 +0200 Subject: [PATCH] fix(notifications): use instance URL in volume backup notification links Volume backup notifications built links with route(), which uses APP_URL in queue jobs and gave http://localhost links. Build them from base_url(). Closes #12285 --- app/Models/ScheduledVolumeBackup.php | 14 ++++++++++++++ app/Notifications/VolumeBackup/BackupFailed.php | 7 +------ .../VolumeBackup/BackupMissing.php | 7 +------ .../VolumeBackup/BackupSuccess.php | 7 +------ .../VolumeBackup/RecoveryFailed.php | 7 +------ tests/Feature/VolumeBackupTest.php | 17 +++++++++++++++++ 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app/Models/ScheduledVolumeBackup.php b/app/Models/ScheduledVolumeBackup.php index 0c4443b2e3..2d71318f93 100644 --- a/app/Models/ScheduledVolumeBackup.php +++ b/app/Models/ScheduledVolumeBackup.php @@ -158,6 +158,20 @@ class ScheduledVolumeBackup extends BaseModel return $this->backupable?->resource; } + /** + * Link to the resource in the Coolify UI. Built from base_url(), because queued notifications have no request and route() would use APP_URL. + */ + public function resourceUrl(): ?string + { + $resource = $this->targetResource(); + $linkable = $resource instanceof ServiceApplication || $resource instanceof ServiceDatabase ? $resource->service : $resource; + if (! $linkable || ! method_exists($linkable, 'link') || ! $link = $linkable->link()) { + return null; + } + + return rtrim(base_url(), '/').parse_url($link, PHP_URL_PATH); + } + public function targetType(): string { return $this->backupable instanceof LocalFileVolume ? 'Directory' : 'Volume'; diff --git a/app/Notifications/VolumeBackup/BackupFailed.php b/app/Notifications/VolumeBackup/BackupFailed.php index 781a7b63e3..c1dc753cd7 100644 --- a/app/Notifications/VolumeBackup/BackupFailed.php +++ b/app/Notifications/VolumeBackup/BackupFailed.php @@ -3,8 +3,6 @@ namespace App\Notifications\VolumeBackup; use App\Models\ScheduledVolumeBackup; -use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; @@ -29,10 +27,7 @@ class BackupFailed extends CustomEmailNotification $this->target = $backup->targetType().' '.$backup->targetName(); $this->frequency = $backup->frequency; - $linkable = $resource instanceof ServiceApplication || $resource instanceof ServiceDatabase ? $resource->service : $resource; - if ($linkable && method_exists($linkable, 'link')) { - $this->url = $linkable->link(); - } + $this->url = $backup->resourceUrl(); } public function via(object $notifiable): array diff --git a/app/Notifications/VolumeBackup/BackupMissing.php b/app/Notifications/VolumeBackup/BackupMissing.php index 63282e3c13..223ee1e0ee 100644 --- a/app/Notifications/VolumeBackup/BackupMissing.php +++ b/app/Notifications/VolumeBackup/BackupMissing.php @@ -3,8 +3,6 @@ namespace App\Notifications\VolumeBackup; use App\Models\ScheduledVolumeBackup; -use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; @@ -27,10 +25,7 @@ class BackupMissing extends CustomEmailNotification $this->resourceName = $resource?->name ?? 'Unknown resource'; $this->target = $backup->targetType().' '.$backup->targetName(); - $linkable = $resource instanceof ServiceApplication || $resource instanceof ServiceDatabase ? $resource->service : $resource; - if ($linkable && method_exists($linkable, 'link')) { - $this->url = $linkable->link(); - } + $this->url = $backup->resourceUrl(); } public function via(object $notifiable): array diff --git a/app/Notifications/VolumeBackup/BackupSuccess.php b/app/Notifications/VolumeBackup/BackupSuccess.php index 4ff192971a..eaa4c823fb 100644 --- a/app/Notifications/VolumeBackup/BackupSuccess.php +++ b/app/Notifications/VolumeBackup/BackupSuccess.php @@ -3,8 +3,6 @@ namespace App\Notifications\VolumeBackup; use App\Models\ScheduledVolumeBackup; -use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; @@ -29,10 +27,7 @@ class BackupSuccess extends CustomEmailNotification $this->target = $backup->targetType().' '.$backup->targetName(); $this->frequency = $backup->frequency; - $linkable = $resource instanceof ServiceApplication || $resource instanceof ServiceDatabase ? $resource->service : $resource; - if ($linkable && method_exists($linkable, 'link')) { - $this->url = $linkable->link(); - } + $this->url = $backup->resourceUrl(); } public function via(object $notifiable): array diff --git a/app/Notifications/VolumeBackup/RecoveryFailed.php b/app/Notifications/VolumeBackup/RecoveryFailed.php index ecb2220ec5..94713f76f5 100644 --- a/app/Notifications/VolumeBackup/RecoveryFailed.php +++ b/app/Notifications/VolumeBackup/RecoveryFailed.php @@ -4,8 +4,6 @@ namespace App\Notifications\VolumeBackup; use App\Jobs\VolumeBackupRecoveryJob; use App\Models\ScheduledVolumeBackupExecution; -use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; @@ -39,10 +37,7 @@ class RecoveryFailed extends CustomEmailNotification }; $this->containersStopped = (bool) $execution->stop_recovery_pending; - $linkable = $resource instanceof ServiceApplication || $resource instanceof ServiceDatabase ? $resource->service : $resource; - if ($linkable && method_exists($linkable, 'link')) { - $this->url = $linkable->link(); - } + $this->url = $backup?->resourceUrl(); } public function via(object $notifiable): array diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index 14b0676aa3..47450f2151 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -2920,6 +2920,23 @@ it('notifies the team once when a volume backup fails', function () { Notification::assertNotSentTo($team, BackupSuccess::class); }); +it('links volume backup notifications to the instance URL instead of APP_URL', function () { + config()->set('app.url', 'http://localhost'); + InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0, 'fqdn' => 'https://coolify.example.com'])); + $team = Team::factory()->create(); + [$application, $volume] = createVolumeBackupApplication($team); + $backup = $volume->scheduledBackups()->create([ + 'team_id' => $team->id, + 'frequency' => 'daily', + ]); + $expected = "https://coolify.example.com/project/{$application->environment->project->uuid}/environment/{$application->environment->uuid}/application/{$application->uuid}"; + + $notification = new BackupFailed($backup, 'Archive command failed'); + + expect($notification->url)->toBe($expected) + ->and($notification->toPushover()->toPayload('token', 'user')['message'])->toContain("href='{$expected}'"); +}); + it('sends volume backup success with a warning through the backup failure channels', function (?string $warning, string $event) { $team = Team::factory()->create(); [$application, $volume] = createVolumeBackupApplication($team);