From 6abbf84520bdfc245c3346661fd6b7fd1b50e113 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:43:07 +0200 Subject: [PATCH 1/5] fix(notifications): build api token expiry notification link from the instance url --- app/Notifications/ApiTokenExpiringNotification.php | 2 +- tests/Feature/ApiTokenExpirationWarningTest.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Notifications/ApiTokenExpiringNotification.php b/app/Notifications/ApiTokenExpiringNotification.php index 451dd312a..40bd3ead0 100644 --- a/app/Notifications/ApiTokenExpiringNotification.php +++ b/app/Notifications/ApiTokenExpiringNotification.php @@ -21,7 +21,7 @@ public function __construct(public PersonalAccessToken $token) $this->onQueue('high'); $this->tokenName = $token->name; $this->expiresAt = $token->expires_at?->format('Y-m-d H:i:s') ?? ''; - $this->manageUrl = route('security.api-tokens'); + $this->manageUrl = base_url().'/security/api-tokens'; } public function via(object $notifiable): array diff --git a/tests/Feature/ApiTokenExpirationWarningTest.php b/tests/Feature/ApiTokenExpirationWarningTest.php index 92c207607..a6bbffbbc 100644 --- a/tests/Feature/ApiTokenExpirationWarningTest.php +++ b/tests/Feature/ApiTokenExpirationWarningTest.php @@ -1,6 +1,7 @@ 0]); $this->team = Team::factory()->create(); $this->user = User::factory()->create(); $this->team->members()->attach($this->user->id, ['role' => 'owner']); @@ -137,4 +139,15 @@ function createTokenExpiring(User $user, Team $team, ?Carbon $expiresAt, ?Carbon Notification::assertNothingSent(); expect($token->fresh()->api_token_expiration_warning_sent_at)->toBeNull(); }); + + test('manage url uses the instance fqdn when configured', function () { + InstanceSettings::query()->update(['fqdn' => 'https://coolify.example.com']); + $token = createTokenExpiring($this->user, $this->team, Carbon::now()->addHours(12)); + + $notification = new ApiTokenExpiringNotification($token); + + expect($notification->toSlack()->description) + ->toContain('https://coolify.example.com/security/api-tokens') + ->not->toContain('localhost'); + }); }); From 306d4833a99f58cb2931b2f35628e9fe9989a0ca Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:45:40 +0200 Subject: [PATCH 2/5] fix(notifications): build restart limit links from the instance url --- .../Application/RestartLimitReached.php | 22 ++++++--- ...pplicationStoppedAfterRestartLimitTest.php | 49 +++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/app/Notifications/Application/RestartLimitReached.php b/app/Notifications/Application/RestartLimitReached.php index 687fd3086..de9de1f98 100644 --- a/app/Notifications/Application/RestartLimitReached.php +++ b/app/Notifications/Application/RestartLimitReached.php @@ -2,8 +2,11 @@ namespace App\Notifications\Application; +use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\BaseModel; +use App\Models\ServiceApplication; +use App\Models\ServiceDatabase; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; @@ -49,14 +52,19 @@ public function __construct(public BaseModel $resource) if (str($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = str($this->fqdn)->explode(',')->first(); } - $service = data_get($resource, 'service'); - $this->resource_url = match (true) { - method_exists($this->resource, 'link') => $this->resource->link(), - $resource instanceof ApplicationPreview => $resource->application->link(), - is_object($service) && method_exists($service, 'link') => $service->link(), - default => null, + $this->resource_url = $this->resolveResourceUrl($resource); + } + + private function resolveResourceUrl(BaseModel $resource): string + { + [$type, $uuid] = match (true) { + $resource instanceof Application => ['application', $resource->uuid], + $resource instanceof ApplicationPreview => ['application', $resource->application->uuid], + $resource instanceof ServiceApplication, $resource instanceof ServiceDatabase => ['service', $resource->service->uuid], + default => ['database', $resource->uuid], }; - $this->resource_url ??= base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}"; + + return base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}/{$type}/{$uuid}"; } public function via(object $notifiable): array diff --git a/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php b/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php index 3e6e04629..58ebb8c70 100644 --- a/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php +++ b/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php @@ -6,10 +6,21 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\BaseModel; +use App\Models\InstanceSettings; use App\Models\Server; +use App\Models\Service; +use App\Models\ServiceApplication; +use App\Models\StandalonePostgresql; use App\Notifications\Application\RestartLimitReached; +use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery\MockInterface; +uses(RefreshDatabase::class); + +beforeEach(function () { + InstanceSettings::forceCreate(['id' => 0, 'fqdn' => 'https://coolify.test']); +}); + function applicationWithRestartState(array $attributes = []): Application { $application = new Application; @@ -161,14 +172,8 @@ function applicationWithRestartState(array $attributes = []): Application ->and($sentinelJob)->toContain('if ($application->stoppedAfterRestartLimit() && $containerStatuses->every('); }); -it('uses the application link for restart limit notifications', function () { - $application = new class extends Application - { - public function link() - { - return 'https://coolify.test/project/link-from-model'; - } - }; +it('builds restart limit notification urls from the instance base url', function () { + $application = new Application; $application->forceFill([ 'name' => 'crashy-app', 'uuid' => 'application-uuid', @@ -183,7 +188,33 @@ public function link() $notification = new RestartLimitReached($application); - expect($notification->resource_url)->toBe('https://coolify.test/project/link-from-model'); + expect($notification->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid'); +}); + +it('links preview, service resource and database restart limit notifications to their pages', function () { + $environment = (object) ['uuid' => 'environment-uuid', 'name' => 'production', 'project' => (object) ['uuid' => 'project-uuid']]; + + $application = new Application; + $application->forceFill(['name' => 'app', 'uuid' => 'application-uuid']); + $application->setRelation('environment', $environment); + $preview = new ApplicationPreview; + $preview->forceFill(['uuid' => 'preview-uuid', 'pull_request_id' => 42, 'restart_count' => 2, 'max_restart_count' => 2]); + $preview->setRelation('application', $application); + + $service = new Service; + $service->forceFill(['uuid' => 'service-uuid']); + $service->setRelation('environment', $environment); + $serviceApplication = new ServiceApplication; + $serviceApplication->forceFill(['name' => 'database', 'uuid' => 'service-application-uuid', 'restart_count' => 2, 'max_restart_count' => 2]); + $serviceApplication->setRelation('service', $service); + + $database = new StandalonePostgresql; + $database->forceFill(['name' => 'postgres', 'uuid' => 'database-uuid', 'restart_count' => 2, 'max_restart_count' => 2]); + $database->setRelation('environment', $environment); + + expect((new RestartLimitReached($preview))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid') + ->and((new RestartLimitReached($serviceApplication))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/service/service-uuid') + ->and((new RestartLimitReached($database))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/database/database-uuid'); }); it('uses the resolved environment project name in Slack restart limit notifications', function () { From 85ec70643f4a5960b5ccce694b8de7b5425a563c Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:48:16 +0200 Subject: [PATCH 3/5] refactor(notifications): build scheduled task links from the instance url - replace the taskLink() helpers on Application and Service with links built from base_url() in the task notifications --- app/Models/Application.php | 26 ------------------ app/Models/Service.php | 27 ------------------- .../ScheduledTask/TaskFailed.php | 9 ++++--- .../ScheduledTask/TaskSuccess.php | 9 ++++--- 4 files changed, 10 insertions(+), 61 deletions(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index f8eb75a5a..6bfbb6de7 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -621,32 +621,6 @@ public function stoppedAfterRestartLimit(): bool && $this->restart_limit_reached === true; } - public function taskLink($task_uuid) - { - if (data_get($this, 'environment.project.uuid')) { - $route = route('project.application.scheduled-tasks', [ - 'project_uuid' => data_get($this, 'environment.project.uuid'), - 'environment_uuid' => data_get($this, 'environment.uuid'), - 'application_uuid' => data_get($this, 'uuid'), - 'task_uuid' => $task_uuid, - ]); - $settings = instanceSettings(); - if (data_get($settings, 'fqdn')) { - $url = Url::fromString($route); - $url = $url->withPort(null); - $fqdn = data_get($settings, 'fqdn'); - $fqdn = str_replace(['http://', 'https://'], '', $fqdn); - $url = $url->withHost($fqdn); - - return $url->__toString(); - } - - return $route; - } - - return null; - } - public function settings() { return $this->hasOne(ApplicationSetting::class); diff --git a/app/Models/Service.php b/app/Models/Service.php index 66c67ca8d..1e6a33ad6 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -15,7 +15,6 @@ use Illuminate\Support\Facades\Storage; use OpenApi\Attributes as OA; use Spatie\Activitylog\Models\Activity; -use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; #[OA\Schema( @@ -1463,32 +1462,6 @@ public function link() return null; } - public function taskLink($task_uuid) - { - if (data_get($this, 'environment.project.uuid')) { - $route = route('project.service.scheduled-tasks', [ - 'project_uuid' => data_get($this, 'environment.project.uuid'), - 'environment_uuid' => data_get($this, 'environment.uuid'), - 'service_uuid' => data_get($this, 'uuid'), - 'task_uuid' => $task_uuid, - ]); - $settings = InstanceSettings::get(); - if (data_get($settings, 'fqdn')) { - $url = Url::fromString($route); - $url = $url->withPort(null); - $fqdn = data_get($settings, 'fqdn'); - $fqdn = str_replace(['http://', 'https://'], '', $fqdn); - $url = $url->withHost($fqdn); - - return $url->__toString(); - } - - return $route; - } - - return null; - } - public function documentation() { $services = get_service_templates(); diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index bd060112a..2ca3874ba 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -2,6 +2,7 @@ namespace App\Notifications\ScheduledTask; +use App\Models\Application; use App\Models\ScheduledTask; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; @@ -16,10 +17,10 @@ class TaskFailed extends CustomEmailNotification public function __construct(public ScheduledTask $task, public string $output) { $this->onQueue('high'); - if ($task->application) { - $this->url = $task->application->taskLink($task->uuid); - } elseif ($task->service) { - $this->url = $task->service->taskLink($task->uuid); + $resource = $task->application ?? $task->service; + if ($resource) { + $type = $resource instanceof Application ? 'application' : 'service'; + $this->url = base_url().'/project/'.data_get($resource, 'environment.project.uuid').'/environment/'.data_get($resource, 'environment.uuid')."/{$type}/{$resource->uuid}/tasks/{$task->uuid}"; } } diff --git a/app/Notifications/ScheduledTask/TaskSuccess.php b/app/Notifications/ScheduledTask/TaskSuccess.php index 58c959bd8..2978eaed3 100644 --- a/app/Notifications/ScheduledTask/TaskSuccess.php +++ b/app/Notifications/ScheduledTask/TaskSuccess.php @@ -2,6 +2,7 @@ namespace App\Notifications\ScheduledTask; +use App\Models\Application; use App\Models\ScheduledTask; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; @@ -16,10 +17,10 @@ class TaskSuccess extends CustomEmailNotification public function __construct(public ScheduledTask $task, public string $output) { $this->onQueue('high'); - if ($task->application) { - $this->url = $task->application->taskLink($task->uuid); - } elseif ($task->service) { - $this->url = $task->service->taskLink($task->uuid); + $resource = $task->application ?? $task->service; + if ($resource) { + $type = $resource instanceof Application ? 'application' : 'service'; + $this->url = base_url().'/project/'.data_get($resource, 'environment.project.uuid').'/environment/'.data_get($resource, 'environment.uuid')."/{$type}/{$resource->uuid}/tasks/{$task->uuid}"; } } From 0eb9d5e5a8952552d8a81bb8472bf4207035776d Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:07:26 +0200 Subject: [PATCH 4/5] fix(notifications): urls for ssl renewal notifications --- app/Jobs/RegenerateSslCertJob.php | 5 ++- .../SslExpirationNotification.php | 37 ++----------------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php index 6f49cf30b..ed2d1c454 100644 --- a/app/Jobs/RegenerateSslCertJob.php +++ b/app/Jobs/RegenerateSslCertJob.php @@ -66,7 +66,10 @@ public function handle() caCert: $caCert->ssl_certificate, caKey: $caCert->ssl_private_key, ); - $regenerated->push($certificate); + $resource = $certificate->database; + if ($resource) { + $regenerated->push($resource); + } } catch (\Exception $e) { Log::error('Failed to regenerate SSL certificate: '.$e->getMessage()); } diff --git a/app/Notifications/SslExpirationNotification.php b/app/Notifications/SslExpirationNotification.php index 78e1e8be9..023031ddd 100644 --- a/app/Notifications/SslExpirationNotification.php +++ b/app/Notifications/SslExpirationNotification.php @@ -7,7 +7,6 @@ use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Collection; -use Spatie\Url\Url; class SslExpirationNotification extends CustomEmailNotification { @@ -19,39 +18,9 @@ public function __construct(array|Collection $resources) { $this->onQueue('high'); $this->resources = collect($resources); - - // Collect URLs for each resource - $this->resources->each(function ($resource) { - if (data_get($resource, 'environment.project.uuid')) { - $routeName = match ($resource->type()) { - 'application' => 'project.application.configuration', - 'database' => 'project.database.configuration', - 'service' => 'project.service.configuration', - default => null - }; - - if ($routeName) { - $route = route($routeName, [ - 'project_uuid' => data_get($resource, 'environment.project.uuid'), - 'environment_uuid' => data_get($resource, 'environment.uuid'), - $resource->type().'_uuid' => data_get($resource, 'uuid'), - ]); - - $settings = instanceSettings(); - if (data_get($settings, 'fqdn')) { - $url = Url::fromString($route); - $url = $url->withPort(null); - $fqdn = data_get($settings, 'fqdn'); - $fqdn = str_replace(['http://', 'https://'], '', $fqdn); - $url = $url->withHost($fqdn); - - $this->urls[$resource->name] = $url->__toString(); - } else { - $this->urls[$resource->name] = $route; - } - } - } - }); + $this->urls = $this->resources->mapWithKeys(fn ($resource) => [ + $resource->name => base_url().'/project/'.data_get($resource, 'environment.project.uuid').'/environment/'.data_get($resource, 'environment.uuid')."/database/{$resource->uuid}", + ])->all(); } public function via(object $notifiable): array From 94995c2a0e8d60ee4adbcb71aab0ff9ecf5855d2 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:08:08 +0200 Subject: [PATCH 5/5] fix(deployments): build deployment log links from the instance url --- app/Jobs/ApplicationDeploymentJob.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 88036f519..01a365a0c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2362,12 +2362,8 @@ private function deploy_to_additional_destinations() destination: $destination, no_questions_asked: true, ); - $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: ".route('project.application.deployment.show', [ - 'project_uuid' => data_get($this->application, 'environment.project.uuid'), - 'application_uuid' => data_get($this->application, 'uuid'), - 'deployment_uuid' => $deployment_uuid, - 'environment_uuid' => data_get($this->application, 'environment.uuid'), - ])); + $deployment_url = base_url().'/project/'.data_get($this->application, 'environment.project.uuid').'/environment/'.data_get($this->application, 'environment.uuid').'/application/'.data_get($this->application, 'uuid')."/deployment/{$deployment_uuid}"; + $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: {$deployment_url}"); } }