feat: add UUIDs and URLs to webhook notifications

- Add resource UUIDs (application_uuid, database_uuid, server_uuid, task_uuid) to all webhook notifications
- Standardize URL field naming from various formats (resource_url, task_url, server_url) to consistent 'url' field
- Include parent resource UUIDs for scheduled tasks (application_uuid or service_uuid)
- Add direct URLs to Coolify resources for all notification types
- Update UI to show "Webhook URL (POST)" label for clarity

This enables webhook consumers to:
- Uniquely identify resources using UUIDs used throughout Coolify UI
- Directly link back to Coolify resource pages via the url field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-10-10 18:41:46 +02:00
parent bdffa4704d
commit 0303f529d3
17 changed files with 354 additions and 29 deletions

View file

@ -185,4 +185,30 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => false,
'message' => 'Deployment failed',
'event' => 'deployment_failed',
'application_name' => $this->application_name,
'application_uuid' => $this->application->uuid,
'deployment_uuid' => $this->deployment_uuid,
'deployment_url' => $this->deployment_url,
'project' => data_get($this->application, 'environment.project.name'),
'environment' => $this->environment_name,
];
if ($this->preview) {
$data['pull_request_id'] = $this->preview->pull_request_id;
$data['preview_fqdn'] = $this->preview->fqdn;
}
if ($this->fqdn) {
$data['fqdn'] = $this->fqdn;
}
return $data;
}
}

View file

@ -205,4 +205,30 @@ public function toSlack(): SlackMessage
color: SlackMessage::successColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => true,
'message' => 'New version successfully deployed',
'event' => 'deployment_success',
'application_name' => $this->application_name,
'application_uuid' => $this->application->uuid,
'deployment_uuid' => $this->deployment_uuid,
'deployment_url' => $this->deployment_url,
'project' => data_get($this->application, 'environment.project.name'),
'environment' => $this->environment_name,
];
if ($this->preview) {
$data['pull_request_id'] = $this->preview->pull_request_id;
$data['preview_fqdn'] = $this->preview->fqdn;
}
if ($this->fqdn) {
$data['fqdn'] = $this->fqdn;
}
return $data;
}
}

View file

@ -113,4 +113,19 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
return [
'success' => false,
'message' => 'Application stopped',
'event' => 'status_changed',
'application_name' => $this->resource_name,
'application_uuid' => $this->resource->uuid,
'url' => $this->resource_url,
'project' => data_get($this->resource, 'environment.project.name'),
'environment' => $this->environment_name,
'fqdn' => $this->fqdn,
];
}
}

View file

@ -102,4 +102,22 @@ public function toSlack(): SlackMessage
color: SlackMessage::warningColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => true,
'message' => 'Resource restarted automatically',
'event' => 'container_restarted',
'container_name' => $this->name,
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
];
if ($this->url) {
$data['url'] = $this->url;
}
return $data;
}
}

View file

@ -102,4 +102,22 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => false,
'message' => 'Resource stopped unexpectedly',
'event' => 'container_stopped',
'container_name' => $this->name,
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
];
if ($this->url) {
$data['url'] = $this->url;
}
return $data;
}
}

View file

@ -88,4 +88,21 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid;
return [
'success' => false,
'message' => 'Database backup failed',
'event' => 'backup_failed',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
'database_type' => $this->database_name,
'frequency' => $this->frequency,
'error_output' => $this->output,
'url' => $url,
];
}
}

View file

@ -85,4 +85,20 @@ public function toSlack(): SlackMessage
color: SlackMessage::successColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid;
return [
'success' => true,
'message' => 'Database backup successful',
'event' => 'backup_success',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
'database_type' => $this->database_name,
'frequency' => $this->frequency,
'url' => $url,
];
}
}

View file

@ -113,4 +113,27 @@ public function toSlack(): SlackMessage
color: SlackMessage::warningColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid;
$data = [
'success' => true,
'message' => 'Database backup succeeded locally, S3 upload failed',
'event' => 'backup_success_with_s3_warning',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
'database_type' => $this->database_name,
'frequency' => $this->frequency,
's3_error' => $this->s3_error,
'url' => $url,
];
if ($this->s3_storage_url) {
$data['s3_storage_url'] = $this->s3_storage_url;
}
return $data;
}
}

View file

@ -114,4 +114,28 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => false,
'message' => 'Scheduled task failed',
'event' => 'task_failed',
'task_name' => $this->task->name,
'task_uuid' => $this->task->uuid,
'output' => $this->output,
];
if ($this->task->application) {
$data['application_uuid'] = $this->task->application->uuid;
} elseif ($this->task->service) {
$data['service_uuid'] = $this->task->service->uuid;
}
if ($this->url) {
$data['url'] = $this->url;
}
return $data;
}
}

View file

@ -105,4 +105,28 @@ public function toSlack(): SlackMessage
color: SlackMessage::successColor()
);
}
public function toWebhook(): array
{
$data = [
'success' => true,
'message' => 'Scheduled task succeeded',
'event' => 'task_success',
'task_name' => $this->task->name,
'task_uuid' => $this->task->uuid,
'output' => $this->output,
];
if ($this->task->application) {
$data['application_uuid'] = $this->task->application->uuid;
} elseif ($this->task->service) {
$data['service_uuid'] = $this->task->service->uuid;
}
if ($this->url) {
$data['url'] = $this->url;
}
return $data;
}
}

View file

@ -66,4 +66,19 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/server/'.$this->server->uuid;
return [
'success' => false,
'message' => 'Docker cleanup job failed',
'event' => 'docker_cleanup_failed',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'error_message' => $this->message,
'url' => $url,
];
}
}

View file

@ -66,4 +66,19 @@ public function toSlack(): SlackMessage
color: SlackMessage::successColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/server/'.$this->server->uuid;
return [
'success' => true,
'message' => 'Docker cleanup job succeeded',
'event' => 'docker_cleanup_success',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'cleanup_message' => $this->message,
'url' => $url,
];
}
}

View file

@ -88,4 +88,18 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
return [
'success' => false,
'message' => 'High disk usage detected',
'event' => 'high_disk_usage',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'disk_usage' => $this->disk_usage,
'threshold' => $this->server_disk_usage_notification_threshold,
'url' => base_url().'/server/'.$this->server->uuid,
];
}
}

View file

@ -74,4 +74,18 @@ public function toSlack(): SlackMessage
color: SlackMessage::successColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/server/'.$this->server->uuid;
return [
'success' => true,
'message' => 'Server revived',
'event' => 'server_reachable',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'url' => $url,
];
}
}

View file

@ -345,4 +345,47 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
// Handle error case
if (isset($this->patchData['error'])) {
return [
'success' => false,
'message' => 'Failed to check patches',
'event' => 'server_patch_check_error',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'os_id' => $this->patchData['osId'] ?? 'unknown',
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
'error' => $this->patchData['error'],
'url' => $this->serverUrl,
];
}
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$updates = $this->patchData['updates'] ?? [];
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
return [
'success' => false,
'message' => 'Server patches available',
'event' => 'server_patch_check',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'total_updates' => $totalUpdates,
'os_id' => $this->patchData['osId'] ?? 'unknown',
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
'updates' => $updates,
'critical_packages_count' => $criticalPackages->count(),
'url' => $this->serverUrl,
];
}
}

View file

@ -82,4 +82,18 @@ public function toSlack(): SlackMessage
color: SlackMessage::errorColor()
);
}
public function toWebhook(): array
{
$url = base_url().'/server/'.$this->server->uuid;
return [
'success' => false,
'message' => 'Server unreachable',
'event' => 'server_unreachable',
'server_name' => $this->server->name,
'server_uuid' => $this->server->uuid,
'url' => $url,
];
}
}

View file

@ -10,24 +10,27 @@
Save
</x-forms.button>
@if ($webhookEnabled)
<x-forms.button canGate="sendTest" :canResource="$settings" class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
<x-forms.button canGate="sendTest" :canResource="$settings"
class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notification
</x-forms.button>
@else
<x-forms.button canGate="sendTest" :canResource="$settings" disabled class="normal-case dark:text-white btn btn-xs no-animation btn-primary">
<x-forms.button canGate="sendTest" :canResource="$settings" disabled
class="normal-case dark:text-white btn btn-xs no-animation btn-primary">
Send Test Notification
</x-forms.button>
@endif
</div>
<div class="w-48">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSaveWebhookEnabled" id="webhookEnabled" label="Enabled" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSaveWebhookEnabled"
id="webhookEnabled" label="Enabled" />
</div>
<div class="flex items-end gap-2">
<span class="px-3 py-2 text-sm font-mono font-semibold rounded btn btn-sm btn-primary no-animation">POST</span>
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Enter a valid HTTP or HTTPS URL. Coolify will send POST requests to this endpoint when events occur."
required id="webhookUrl" label="Webhook URL" />
required id="webhookUrl" label="Webhook URL (POST)" />
</div>
</form>
<h2 class="mt-4">Notification Settings</h2>
@ -38,10 +41,10 @@
<div class="border dark:border-coolgray-300 border-neutral-200 p-4 rounded-lg">
<h3 class="font-medium mb-3">Deployments</h3>
<div class="flex flex-col gap-1.5 pl-1">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentSuccessWebhookNotifications"
label="Deployment Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentFailureWebhookNotifications"
label="Deployment Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="deploymentSuccessWebhookNotifications" label="Deployment Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="deploymentFailureWebhookNotifications" label="Deployment Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
helper="Send a notification when a container status changes. It will notify for Stopped and Restarted events of a container."
id="statusChangeWebhookNotifications" label="Container Status Changes" />
@ -50,36 +53,36 @@
<div class="border dark:border-coolgray-300 border-neutral-200 p-4 rounded-lg">
<h3 class="font-medium mb-3">Backups</h3>
<div class="flex flex-col gap-1.5 pl-1">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupSuccessWebhookNotifications"
label="Backup Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupFailureWebhookNotifications"
label="Backup Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="backupSuccessWebhookNotifications" label="Backup Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="backupFailureWebhookNotifications" label="Backup Failure" />
</div>
</div>
<div class="border dark:border-coolgray-300 border-neutral-200 p-4 rounded-lg">
<h3 class="font-medium mb-3">Scheduled Tasks</h3>
<div class="flex flex-col gap-1.5 pl-1">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskSuccessWebhookNotifications"
label="Scheduled Task Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskFailureWebhookNotifications"
label="Scheduled Task Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="scheduledTaskSuccessWebhookNotifications" label="Scheduled Task Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="scheduledTaskFailureWebhookNotifications" label="Scheduled Task Failure" />
</div>
</div>
<div class="border dark:border-coolgray-300 border-neutral-200 p-4 rounded-lg">
<h3 class="font-medium mb-3">Server</h3>
<div class="flex flex-col gap-1.5 pl-1">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupSuccessWebhookNotifications"
label="Docker Cleanup Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupFailureWebhookNotifications"
label="Docker Cleanup Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverDiskUsageWebhookNotifications"
label="Server Disk Usage" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverReachableWebhookNotifications"
label="Server Reachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverUnreachableWebhookNotifications"
label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchWebhookNotifications"
label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="dockerCleanupSuccessWebhookNotifications" label="Docker Cleanup Success" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="dockerCleanupFailureWebhookNotifications" label="Docker Cleanup Failure" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="serverDiskUsageWebhookNotifications" label="Server Disk Usage" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="serverReachableWebhookNotifications" label="Server Reachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="serverUnreachableWebhookNotifications" label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="serverPatchWebhookNotifications" label="Server Patching" />
</div>
</div>
</div>