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
This commit is contained in:
Andras Bacsai
2026-10-23 20:05:23 +02:00
parent 30d840f1c6
commit 6022073519
6 changed files with 35 additions and 24 deletions
+14
View File
@@ -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';
@@ -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
@@ -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
@@ -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
@@ -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
+17
View File
@@ -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);