Merge pull request #33 from peaklabs-dev/next

Next into notifications
This commit is contained in:
🏔️ Peak authored and GitHub committed 2024-12-06 14:51:15 +01:00
commit 38aa46bc6b
51 files changed
+1000 -282

No files matched your search

+8 -2
View File
@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
if (isDev()) {
if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToSlackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private SlackMessage $message,
private string $webhookUrl
) {
$this->onQueue('high');
}
public function handle(): void
{
Http::post($this->webhookUrl, [
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'plain_text',
'text' => 'Coolify Notification',
],
],
],
'attachments' => [
[
'color' => $this->message->color,
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => $this->message->title,
],
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $this->message->description,
],
],
],
],
],
]);
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Slack extends Component
{
public Team $team;
#[Validate(['boolean'])]
public bool $slackEnabled = false;
#[Validate(['url', 'nullable'])]
public ?string $slackWebhookUrl = null;
#[Validate(['boolean'])]
public bool $slackNotificationsTest = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDeployments = false;
#[Validate(['boolean'])]
public bool $slackNotificationsStatusChanges = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDatabaseBackups = false;
#[Validate(['boolean'])]
public bool $slackNotificationsScheduledTasks = false;
#[Validate(['boolean'])]
public bool $slackNotificationsServerDiskUsage = false;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->team->slack_enabled = $this->slackEnabled;
$this->team->slack_webhook_url = $this->slackWebhookUrl;
$this->team->slack_notifications_test = $this->slackNotificationsTest;
$this->team->slack_notifications_deployments = $this->slackNotificationsDeployments;
$this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges;
$this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups;
$this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks;
$this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage;
$this->team->save();
refreshSession();
} else {
$this->slackEnabled = $this->team->slack_enabled;
$this->slackWebhookUrl = $this->team->slack_webhook_url;
$this->slackNotificationsTest = $this->team->slack_notifications_test;
$this->slackNotificationsDeployments = $this->team->slack_notifications_deployments;
$this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes;
$this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups;
$this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks;
$this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage;
}
}
public function instantSaveSlackEnabled()
{
try {
$this->validate([
'slackWebhookUrl' => 'required',
], [
'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->slackEnabled = false;
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
try {
$this->team->notify(new Test);
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.notifications.slack');
}
}
+18 -3
View File
@@ -15,6 +15,8 @@ class Proxy extends Component
public $proxy_settings = null;
public bool $redirect_enabled = true;
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
@@ -26,6 +28,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxyType();
$this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
@@ -38,7 +41,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyChanged');
$this->dispatch('reloadWindow');
}
public function selectProxy($proxy_type)
@@ -46,7 +49,7 @@ class Proxy extends Component
try {
$this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type;
$this->dispatch('proxyStatusUpdated');
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -63,13 +66,25 @@ class Proxy extends Component
}
}
public function instantSaveRedirect()
{
try {
$this->server->proxy->redirect_enabled = $this->redirect_enabled;
$this->server->save();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
+2 -1
View File
@@ -65,7 +65,7 @@ class Deploy extends Component
public function restart()
{
try {
$this->stop(forceStop: false);
$this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -105,6 +105,7 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
+36 -10
View File
@@ -1321,17 +1321,43 @@ class Application extends BaseModel
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
if (version_compare($gitVersion, '2.35.1', '<')) {
$fileList = $fileList->map(function ($file) {
$parts = explode('/', trim($file, '.'));
$paths = collect();
$currentPath = '';
foreach ($parts as $part) {
$currentPath .= ($currentPath ? '/' : '').$part;
$paths->push($currentPath);
}
return $paths;
})->flatten()->unique()->values();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
} else {
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
}
try {
$composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) {
+77 -76
View File
@@ -105,6 +105,14 @@ class Server extends BaseModel
]);
}
}
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::retrieved(function ($server) {
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::forceDeleting(function ($server) {
@@ -184,73 +192,80 @@ class Server extends BaseModel
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
public function setupDefault404Redirect()
public function setupDefaultRedirect()
{
$banner =
"# This file is generated by Coolify, do not edit it manually.\n".
"# Disable the default redirect to customize (only if you know what are you doing).\n\n";
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
$redirect_enabled = $this->proxy->redirect_enabled ?? true;
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if (isDev()) {
if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
$this->reloadCaddy();
return;
$dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $default_redirect_file",
], $this);
return;
}
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'HostRegexp(`.+`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp',
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy";
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $dynamic_conf_path/default_redirect_404.yaml",
"rm -f $dynamic_conf_path/default_redirect_404.caddy",
], $this);
if ($redirect_enabled === false) {
instant_remote_process(["rm -f $default_redirect_file"], $this);
} else {
if ($proxy_type === ProxyTypes::CADDY->value) {
if (filled($redirect_url)) {
$conf = ":80, :443 {
redir $redirect_url
}";
} else {
$conf = ':80, :443 {
respond 503
}';
}
} elseif ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'PathPrefix(`/`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => -1000,
],
],
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => '',
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [],
],
],
],
],
'middlewares' => [
];
if (filled($redirect_url)) {
$dynamic_conf['http']['routers']['catchall']['middlewares'] = [
0 => 'redirect-regexp',
];
$dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [
'url' => '',
];
$dynamic_conf['http']['middlewares'] = [
'redirect-regexp' => [
'redirectRegex' => [
'regex' => '(.*)',
@@ -258,32 +273,17 @@ respond 404
'permanent' => false,
],
],
],
],
];
$conf = Yaml::dump($dynamic_conf, 12, 2);
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
];
}
$conf = Yaml::dump($dynamic_conf, 12, 2);
}
$conf = $banner.$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -612,6 +612,7 @@ $schema://$host {
$memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) {
$usedPercent = $metric['usedPercent'] ?? 0.0;
return [(int) $metric['time'], (float) $usedPercent];
});
+8 -2
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsSlack;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@@ -70,7 +71,7 @@ use OpenApi\Attributes as OA;
),
]
)]
class Team extends Model implements SendsDiscord, SendsEmail
class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{
use Notifiable;
@@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail
];
}
public function routeNotificationForSlack()
{
return data_get($this, 'slack_webhook_url', null);
}
public function getRecepients($notification)
{
$recipients = data_get($notification, 'emails', null);
@@ -161,7 +167,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
return 9999999;
}
$team = Team::find(currentTeam()->id);
if (! $team) {
if (!$team) {
return 0;
}
@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class DeploymentFailed extends CustomEmailNotification
@@ -39,7 +40,7 @@ class DeploymentFailed extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array
@@ -53,10 +54,10 @@ class DeploymentFailed extends CustomEmailNotification
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject('Coolify: Deployment failed of '.$this->application_name.'.');
$mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.');
} else {
$fqdn = $this->preview->fqdn;
$mail->subject('Coolify: Deployment failed of pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.'.');
$mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
}
$mail->view('emails.application-deployment-failed', [
'name' => $this->application_name,
@@ -73,7 +74,7 @@ class DeploymentFailed extends CustomEmailNotification
if ($this->preview) {
$message = new DiscordMessage(
title: ':cross_mark: Deployment failed',
description: 'Pull request: '.$this->preview->pull_request_id,
description: 'Pull request: ' . $this->preview->pull_request_id,
color: DiscordMessage::errorColor(),
isCritical: true,
);
@@ -82,13 +83,13 @@ class DeploymentFailed extends CustomEmailNotification
$message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true);
$message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
$message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')');
if ($this->fqdn) {
$message->addField('Domain', $this->fqdn, true);
}
} else {
if ($this->fqdn) {
$description = '[Open application]('.$this->fqdn.')';
$description = '[Open application](' . $this->fqdn . ')';
} else {
$description = '';
}
@@ -103,7 +104,7 @@ class DeploymentFailed extends CustomEmailNotification
$message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true);
$message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
$message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')');
}
return $message;
@@ -112,9 +113,9 @@ class DeploymentFailed extends CustomEmailNotification
public function toTelegram(): array
{
if ($this->preview) {
$message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: ';
$message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: ';
} else {
$message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): ';
$message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): ';
}
$buttons[] = [
'text' => 'Deployment logs',
@@ -128,4 +129,31 @@ class DeploymentFailed extends CustomEmailNotification
],
];
}
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} deployment failed";
$description = "Pull request deployment failed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = "Deployment failed";
$description = "Deployment failed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
@@ -7,6 +7,7 @@ use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class DeploymentSuccess extends CustomEmailNotification
{
@@ -39,7 +40,7 @@ class DeploymentSuccess extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array
@@ -79,21 +80,21 @@ class DeploymentSuccess extends CustomEmailNotification
if ($this->preview) {
$message = new DiscordMessage(
title: ':white_check_mark: Preview deployment successful',
description: 'Pull request: '.$this->preview->pull_request_id,
description: 'Pull request: ' . $this->preview->pull_request_id,
color: DiscordMessage::successColor(),
);
if ($this->preview->fqdn) {
$message->addField('Application', '[Link]('.$this->preview->fqdn.')');
$message->addField('Application', '[Link](' . $this->preview->fqdn . ')');
}
$message->addField('Project', data_get($this->application, 'environment.project.name'), true);
$message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true);
$message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
$message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')');
} else {
if ($this->fqdn) {
$description = '[Open application]('.$this->fqdn.')';
$description = '[Open application](' . $this->fqdn . ')';
} else {
$description = '';
}
@@ -106,7 +107,7 @@ class DeploymentSuccess extends CustomEmailNotification
$message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true);
$message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
$message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')');
}
return $message;
@@ -115,7 +116,7 @@ class DeploymentSuccess extends CustomEmailNotification
public function toTelegram(): array
{
if ($this->preview) {
$message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.'';
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) {
$buttons[] = [
'text' => 'Open Application',
@@ -123,7 +124,7 @@ class DeploymentSuccess extends CustomEmailNotification
];
}
} else {
$message = '✅ New version successfully deployed of '.$this->application_name.'';
$message = '✅ New version successfully deployed of ' . $this->application_name . '';
if ($this->fqdn) {
$buttons[] = [
'text' => 'Open Application',
@@ -143,4 +144,32 @@ class DeploymentSuccess extends CustomEmailNotification
],
];
}
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} successfully deployed";
$description = "New version successfully deployed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = "New version successfully deployed";
$description = "New version successfully deployed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
}
@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class StatusChanged extends CustomEmailNotification
{
@@ -29,7 +30,7 @@ class StatusChanged extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}";
$this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}";
}
public function via(object $notifiable): array
@@ -55,7 +56,7 @@ class StatusChanged extends CustomEmailNotification
{
return new DiscordMessage(
title: ':cross_mark: Application stopped',
description: '[Open Application in Coolify]('.$this->resource_url.')',
description: '[Open Application in Coolify](' . $this->resource_url . ')',
color: DiscordMessage::errorColor(),
isCritical: true,
);
@@ -63,7 +64,7 @@ class StatusChanged extends CustomEmailNotification
public function toTelegram(): array
{
$message = 'Coolify: '.$this->resource_name.' has been stopped.';
$message = 'Coolify: ' . $this->resource_name . ' has been stopped.';
return [
'message' => $message,
@@ -75,4 +76,20 @@ class StatusChanged extends CustomEmailNotification
],
];
}
public function toSlack(): SlackMessage
{
$title = "Application stopped";
$description = "{$this->resource_name} has been stopped";
$description .= "\n\n**Project:** " . data_get($this->resource, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Application URL:** {$this->resource_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
@@ -0,0 +1,8 @@
<?php
namespace App\Notifications\Channels;
interface SendsSlack
{
public function routeNotificationForSlack();
}
@@ -0,0 +1,22 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToSlackJob;
use Illuminate\Notifications\Notification;
class SlackChannel
{
/**
* Send the given notification.
*/
public function send(SendsSlack $notifiable, Notification $notification): void
{
$message = $notification->toSlack();
$webhookUrl = $notifiable->routeNotificationForSlack();
if (! $webhookUrl) {
return;
}
SendMessageToSlackJob::dispatch($message, $webhookUrl);
}
}
@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class ContainerRestarted extends CustomEmailNotification
{
@@ -41,7 +42,7 @@ class ContainerRestarted extends CustomEmailNotification
);
if ($this->url) {
$message->addField('Resource', '[Link]('.$this->url.')');
$message->addField('Resource', '[Link](' . $this->url . ')');
}
return $message;
@@ -66,4 +67,20 @@ class ContainerRestarted extends CustomEmailNotification
return $payload;
}
public function toSlack(): SlackMessage
{
$title = "Resource restarted";
$description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::warningColor()
);
}
}
@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class ContainerStopped extends CustomEmailNotification
{
@@ -41,7 +42,7 @@ class ContainerStopped extends CustomEmailNotification
);
if ($this->url) {
$message->addField('Resource', '[Link]('.$this->url.')');
$message->addField('Resource', '[Link](' . $this->url . ')');
}
return $message;
@@ -66,4 +67,20 @@ class ContainerStopped extends CustomEmailNotification
return $payload;
}
public function toSlack(): SlackMessage
{
$title = "Resource stopped";
$description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
@@ -6,6 +6,7 @@ use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class BackupFailed extends CustomEmailNotification
{
@@ -62,4 +63,19 @@ class BackupFailed extends CustomEmailNotification
'message' => $message,
];
}
public function toSlack(): SlackMessage
{
$title = "Database backup failed";
$description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
$description .= "\n\n**Frequency:** {$this->frequency}";
$description .= "\n\n**Error Output:**\n{$this->output}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
@@ -6,6 +6,7 @@ use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class BackupSuccess extends CustomEmailNotification
{
@@ -60,4 +61,18 @@ class BackupSuccess extends CustomEmailNotification
'message' => $message,
];
}
public function toSlack(): SlackMessage
{
$title = "Database backup successful";
$description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
$description .= "\n\n**Frequency:** {$this->frequency}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Notifications\Dto;
class SlackMessage
{
public function __construct(
public string $title,
public string $description,
public string $color = '#0099ff'
) {
}
public static function infoColor(): string
{
return '#0099ff';
}
public static function errorColor(): string
{
return '#ff0000';
}
public static function successColor(): string
{
return '#00ff00';
}
public static function warningColor(): string
{
return '#ffa500';
}
}
@@ -4,7 +4,9 @@ namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -25,6 +27,7 @@ class GeneralNotification extends Notification implements ShouldQueue
$channels = [];
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
@@ -32,6 +35,9 @@ class GeneralNotification extends Notification implements ShouldQueue
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -51,4 +57,13 @@ class GeneralNotification extends Notification implements ShouldQueue
'message' => $this->message,
];
}
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Coolify: General Notification',
description: $this->message,
color: SlackMessage::infoColor(),
);
}
}
+22 -1
View File
@@ -6,6 +6,7 @@ use App\Models\ScheduledTask;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
use App\Notifications\Dto\SlackMessage;
class TaskFailed extends CustomEmailNotification
{
@@ -48,7 +49,7 @@ class TaskFailed extends CustomEmailNotification
);
if ($this->url) {
$message->addField('Scheduled task', '[Link]('.$this->url.')');
$message->addField('Scheduled task', '[Link](' . $this->url . ')');
}
return $message;
@@ -68,4 +69,24 @@ class TaskFailed extends CustomEmailNotification
'message' => $message,
];
}
public function toSlack(): SlackMessage
{
$title = "Scheduled task failed";
$description = "Scheduled task ({$this->task->name}) failed.";
if ($this->output) {
$description .= "\n\n**Error Output:**\n{$this->output}";
}
if ($this->url) {
$description .= "\n\n**Task URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
+14 -1
View File
@@ -7,6 +7,7 @@ use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
class DockerCleanup extends CustomEmailNotification
{
@@ -21,7 +22,7 @@ class DockerCleanup extends CustomEmailNotification
// $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -31,6 +32,9 @@ class DockerCleanup extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -62,4 +66,13 @@ class DockerCleanup extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
];
}
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server cleanup job done',
description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
color: SlackMessage::successColor()
);
}
}
+21 -1
View File
@@ -6,6 +6,8 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\SlackMessage;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
@@ -23,7 +25,7 @@ class ForceDisabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -33,6 +35,9 @@ class ForceDisabled extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -67,4 +72,19 @@ class ForceDisabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
];
}
public function toSlack(): SlackMessage
{
$title = "Server disabled";
$description = "Server ({$this->server->name}) disabled because it is not paid!\n";
$description .= "All automations and integrations are stopped.\n\n";
$description .= "Please update your subscription to enable the server again: https://app.coolify.io/subscriptions";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
}
+17 -1
View File
@@ -6,6 +6,8 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\SlackMessage;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
@@ -23,7 +25,7 @@ class ForceEnabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -33,6 +35,9 @@ class ForceEnabled extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -63,4 +68,15 @@ class ForceEnabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) enabled again!",
];
}
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server enabled',
description: "Server '{$this->server->name}' enabled again!",
color: SlackMessage::successColor()
);
}
}
+20 -1
View File
@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class HighDiskUsage extends CustomEmailNotification
@@ -44,7 +45,7 @@ class HighDiskUsage extends CustomEmailNotification
$message->addField('Disk usage', "{$this->disk_usage}%", true);
$message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true);
$message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true);
$message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)');
$message->addField('Change Settings', '[Threshold](' . base_url() . '/server/' . $this->server->uuid . '#advanced) | [Notification](' . base_url() . '/notifications/discord)');
return $message;
}
@@ -55,4 +56,22 @@ class HighDiskUsage extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
];
}
public function toSlack(): SlackMessage
{
$description = "Server '{$this->server->name}' high disk usage detected!\n";
$description .= "Disk usage: {$this->disk_usage}%\n";
$description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n";
$description .= "Please cleanup your disk to prevent data-loss.\n";
$description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n";
$description .= "Change settings:\n";
$description .= "- Threshold: " . base_url() . "/server/" . $this->server->uuid . "#advanced\n";
$description .= "- Notifications: " . base_url() . "/notifications/discord";
return new SlackMessage(
title: 'High disk usage detected',
description: $description,
color: SlackMessage::errorColor()
);
}
}
+19 -2
View File
@@ -6,6 +6,8 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\SlackMessage;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
@@ -18,7 +20,7 @@ class Reachable extends CustomEmailNotification
{
$this->onQueue('high');
$this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-reachable:'.$this->server->id,
limiterKey: 'server-reachable:' . $this->server->id,
);
}
@@ -32,7 +34,7 @@ class Reachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -42,6 +44,9 @@ class Reachable extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -72,4 +77,16 @@ class Reachable extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!",
];
}
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: "Server revived",
description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!",
color: SlackMessage::successColor()
);
}
}
+21 -1
View File
@@ -6,6 +6,8 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\SlackMessage;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Notifications\Messages\MailMessage;
@@ -18,7 +20,7 @@ class Unreachable extends CustomEmailNotification
{
$this->onQueue('high');
$this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-unreachable:'.$this->server->id,
limiterKey: 'server-unreachable:' . $this->server->id,
);
}
@@ -32,6 +34,7 @@ class Unreachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
@@ -42,6 +45,9 @@ class Unreachable extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -76,4 +82,18 @@ class Unreachable extends CustomEmailNotification
'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.",
];
}
public function toSlack(): SlackMessage
{
$description = "Your server '{$this->server->name}' is unreachable.\n";
$description .= "All automations & integrations are turned off!\n\n";
$description .= "*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations.";
return new SlackMessage(
title: 'Server unreachable',
description: $description,
color: SlackMessage::errorColor()
);
}
}
+9
View File
@@ -3,6 +3,7 @@
namespace App\Notifications;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -67,4 +68,12 @@ class Test extends Notification implements ShouldQueue
],
];
}
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Test Slack Notification',
description: 'This is a test Slack notification from Coolify.'
);
}
}
+9 -5
View File
@@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server)
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}:/traefik",
],
'command' => [
'--ping=true',
'--ping.entrypoint=http',
'--api.dashboard=true',
'--api.insecure=false',
'--entrypoints.http.address=:80',
'--entrypoints.https.address=:443',
'--entrypoints.http.http.encodequerysemicolons=true',
@@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server)
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
'--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
'--providers.docker.exposedbydefault=false',
'--providers.file.watch=true',
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
],
'labels' => $labels,
],
],
];
if (isDev()) {
// $config['services']['traefik']['command'][] = "--log.level=debug";
$config['services']['traefik']['command'][] = '--api.insecure=true';
$config['services']['traefik']['command'][] = '--log.level=debug';
$config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log';
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
$config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik';
} else {
$config['services']['traefik']['command'][] = '--api.insecure=false';
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
}
if ($server->isSwarm()) {
data_forget($config, 'services.traefik.container_name');
+137 -131
View File
@@ -27,6 +27,7 @@ use App\Models\Team;
use App\Models\User;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
use Carbon\CarbonImmutable;
@@ -67,27 +68,27 @@ function base_configuration_dir(): string
}
function application_configuration_dir(): string
{
return base_configuration_dir().'/applications';
return base_configuration_dir() . '/applications';
}
function service_configuration_dir(): string
{
return base_configuration_dir().'/services';
return base_configuration_dir() . '/services';
}
function database_configuration_dir(): string
{
return base_configuration_dir().'/databases';
return base_configuration_dir() . '/databases';
}
function database_proxy_dir($uuid): string
{
return base_configuration_dir()."/databases/$uuid/proxy";
return base_configuration_dir() . "/databases/$uuid/proxy";
}
function backup_dir(): string
{
return base_configuration_dir().'/backups';
return base_configuration_dir() . '/backups';
}
function metrics_dir(): string
{
return base_configuration_dir().'/metrics';
return base_configuration_dir() . '/metrics';
}
function sanitize_string(?string $input = null): ?string
@@ -138,15 +139,15 @@ function showBoarding(): bool
}
function refreshSession(?Team $team = null): void
{
if (! $team) {
if (!$team) {
if (Auth::user()->currentTeam()) {
$team = Team::find(Auth::user()->currentTeam()->id);
} else {
$team = User::find(Auth::id())->teams->first();
}
}
Cache::forget('team:'.Auth::id());
Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
Cache::forget('team:' . Auth::id());
Cache::remember('team:' . Auth::id(), 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
@@ -178,7 +179,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
$message = null;
}
if ($customErrorMessage) {
$message = $customErrorMessage.' '.$message;
$message = $customErrorMessage . ' ' . $message;
}
if (isset($livewire)) {
@@ -251,7 +252,7 @@ function generateSSHKey(string $type = 'rsa')
function formatPrivateKey(string $privateKey)
{
$privateKey = trim($privateKey);
if (! str_ends_with($privateKey, "\n")) {
if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n";
}
@@ -273,7 +274,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{
if (! $settings) {
if (!$settings) {
$settings = instanceSettings();
}
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
@@ -373,7 +374,7 @@ function isSubscribed()
function isProduction(): bool
{
return ! isDev();
return !isDev();
}
function isDev(): bool
{
@@ -426,14 +427,14 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
{
$settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
if (!$type) {
throw new Exception('No email settings found.');
}
if ($cc) {
Mail::send(
[],
[],
fn (Message $message) => $message
fn(Message $message) => $message
->to($email)
->replyTo($email)
->cc($cc)
@@ -444,7 +445,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
Mail::send(
[],
[],
fn (Message $message) => $message
fn(Message $message) => $message
->to($email)
->subject($mail->subject)
->html((string) $mail->render())
@@ -469,11 +470,13 @@ function setNotificationChannels($notifiable, $event)
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
$isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
@@ -484,6 +487,9 @@ function setNotificationChannels($notifiable, $event)
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class;
}
if ($isSlackEnabled && $isSubscribedToSlackEvent) {
$channels[] = SlackChannel::class;
}
return $channels;
}
@@ -588,7 +594,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
return null;
}
$resource = queryResourcesByUuid($uuid);
if (! is_null($resource) && $resource->environment->project->team_id === $teamId) {
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource;
}
@@ -680,29 +686,29 @@ function queryResourcesByUuid(string $uuid)
function generateTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = "/deploy?tag=$tag_name";
return $api.$endpoint;
return $api . $endpoint;
}
function generateDeployWebhook($resource)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy';
$uuid = data_get($resource, 'uuid');
return $api.$endpoint."?uuid=$uuid&force=false";
return $api . $endpoint . "?uuid=$uuid&force=false";
}
function generateGitManualWebhook($resource, $type)
{
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) {
if ($resource->source_id !== 0 && !is_null($resource->source_id)) {
return null;
}
if ($resource->getMorphClass() === \App\Models\Application::class) {
$baseUrl = base_url();
return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
return Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
}
return null;
@@ -729,7 +735,7 @@ function getTopLevelNetworks(Service|Application $resource)
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
// Only add 'networks' key if 'network_mode' is not 'host'
if (! $hasHostNetworkMode) {
if (!$hasHostNetworkMode) {
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
@@ -743,7 +749,7 @@ function getTopLevelNetworks(Service|Application $resource)
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
@@ -754,7 +760,7 @@ function getTopLevelNetworks(Service|Application $resource)
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (! $definedNetworkExists) {
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
@@ -795,7 +801,7 @@ function getTopLevelNetworks(Service|Application $resource)
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
@@ -805,7 +811,7 @@ function getTopLevelNetworks(Service|Application $resource)
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (! $definedNetworkExists) {
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
@@ -941,7 +947,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
// This is not base64, it's just a random string
// This is not base64, it's just a random string
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
@@ -952,7 +958,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'BASE64_32':
$generatedValue = Str::random(32);
break;
// This is base64,
// This is base64,
case 'REALBASE64_64':
$generatedValue = base64_encode(Str::random(64));
break;
@@ -1052,7 +1058,7 @@ function validate_dns_entry(string $fqdn, Server $server)
}
$settings = instanceSettings();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (! $is_dns_validation_enabled) {
if (!$is_dns_validation_enabled) {
return true;
}
$dns_servers = data_get($settings, 'custom_dns_servers');
@@ -1070,7 +1076,7 @@ function validate_dns_entry(string $fqdn, Server $server)
$query = new DNSQuery($dns_server);
$results = $query->query($host, $type);
if ($results === false || $query->hasError()) {
ray('Error: '.$query->getLasterror());
ray('Error: ' . $query->getLasterror());
} else {
foreach ($results as $result) {
if ($result->getType() == $type) {
@@ -1080,7 +1086,7 @@ function validate_dns_entry(string $fqdn, Server $server)
break;
}
if ($result->getData() === $ip) {
ray($host.' has IP address '.$result->getData());
ray($host . ' has IP address ' . $result->getData());
ray($result->getString());
$found_matching_ip = true;
break;
@@ -1128,15 +1134,15 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
$serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
if ($uuid) {
$applications = $applications->filter(fn ($app) => $app->uuid !== $uuid);
$serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid);
$applications = $applications->filter(fn($app) => $app->uuid !== $uuid);
$serviceApplications = $serviceApplications->filter(fn($app) => $app->uuid !== $uuid);
}
$domainFound = false;
foreach ($applications as $app) {
if (is_null($app->fqdn)) {
continue;
}
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
@@ -1155,7 +1161,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
if (str($app->fqdn)->isEmpty()) {
continue;
}
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
@@ -1205,7 +1211,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
@@ -1224,7 +1230,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
}
$apps = ServiceApplication::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
@@ -1260,7 +1266,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{
$commands = $commands->map(function ($line) {
if (
! str(trim($line))->startsWith([
!str(trim($line))->startsWith([
'cd',
'command',
'echo',
@@ -1281,7 +1287,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
$commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p');
}
return $line;
@@ -1309,11 +1315,11 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
}
function parseLineForSudo(string $command, Server $server): string
{
if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) {
if (!str($command)->startSwith('cd') && !str($command)->startSwith('command')) {
$command = "sudo $command";
}
if (Str::startsWith($command, 'sudo mkdir -p')) {
$command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p');
$command = "$command && sudo chown -R $server->user:$server->user " . Str::after($command, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($command, 'sudo mkdir -p');
}
if (str($command)->contains('$(') || str($command)->contains('`')) {
$command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
@@ -1435,7 +1441,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
$isDirectory = data_get($foundConfig, 'is_directory');
} else {
$isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) {
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
ray('setting isDirectory to true');
$isDirectory = true;
@@ -1450,9 +1456,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
return $volume;
}
if (get_class($resource) === \App\Models\Application::class) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
$dir = base_configuration_dir() . '/applications/' . $resource->uuid;
} else {
$dir = base_configuration_dir().'/services/'.$resource->service->uuid;
$dir = base_configuration_dir() . '/services/' . $resource->service->uuid;
}
if ($source->startsWith('.')) {
@@ -1462,9 +1468,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
$source = $source->replaceFirst('~', $dir);
}
if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id";
$source = $source . "-pr-$pull_request_id";
}
if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) {
if (!$resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) {
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
@@ -1593,7 +1599,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (! str($serviceLabel)->contains('=')) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
@@ -1675,7 +1681,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
@@ -1701,12 +1707,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
if (! $hasHostNetworkMode) {
if (!$hasHostNetworkMode) {
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (! $definedNetworkExists) {
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
@@ -1918,9 +1924,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdn = "$fqdn$path";
}
if (! $isDatabase) {
if (!$isDatabase) {
if ($savedService->fqdn) {
data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn);
data_set($savedService, 'fqdn', $savedService->fqdn . ',' . $fqdn);
} else {
data_set($savedService, 'fqdn', $fqdn);
}
@@ -1935,7 +1941,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]);
}
// Caddy needs exact port in some cases.
if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) {
if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) {
$fqdns_exploded = str($savedService->fqdn)->explode(',');
if ($fqdns_exploded->count() > 1) {
continue;
@@ -1975,12 +1981,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if (! is_null($command)) {
if (!is_null($command)) {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($resource->server, $containerName);
} else {
$fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid);
$fqdn = generateFqdn($resource->server, Str::lower($forService) . '-' . $resource->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
@@ -2010,13 +2016,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'is_preview' => false,
]);
}
if (! $isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) {
if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
// Caddy needs exact port in some cases.
if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') {
if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') {
$fqdns_exploded = str($savedService->fqdn)->explode(',');
if ($fqdns_exploded->count() > 1) {
continue;
@@ -2038,7 +2044,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} else {
$generatedValue = generateEnvValue($command, $resource);
if (! $foundEnv) {
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
@@ -2093,7 +2099,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
$defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (! $isDatabase && $fqdns->count() > 0) {
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
if ($shouldGenerateLabelsExactly) {
@@ -2161,7 +2167,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (! data_get($service, 'restart')) {
if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE);
}
if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) {
@@ -2200,21 +2206,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}");
// TODO: move this in a shared function
if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) {
if (!$parsedServiceVariables->has('COOLIFY_APP_NAME')) {
$parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\"");
}
if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) {
if (!$parsedServiceVariables->has('COOLIFY_SERVER_IP')) {
$parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\"");
}
if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) {
if (!$parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) {
$parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\"");
}
if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) {
if (!$parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) {
$parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\"");
}
$parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) {
if (! str($value)->startsWith('$')) {
if (!str($value)->startsWith('$')) {
$found_env = $envs_from_coolify->where('key', $key)->first();
if ($found_env) {
return $found_env->value;
@@ -2298,7 +2304,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (! str($serviceLabel)->contains('=')) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
@@ -2318,11 +2324,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
if (is_string($volume)) {
$volume = str($volume);
if ($volume->contains(':') && ! $volume->startsWith('/')) {
if ($volume->contains(':') && !$volume->startsWith('/')) {
$name = $volume->before(':');
$mount = $volume->after(':');
if ($name->startsWith('.') || $name->startsWith('~')) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
$dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if ($name->startsWith('.')) {
$name = $name->replaceFirst('.', $dir);
}
@@ -2330,12 +2336,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir);
}
if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id";
$name = $name . "-pr-$pull_request_id";
}
$volume = str("$name:$mount");
} else {
if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id";
$name = $name . "-pr-$pull_request_id";
$volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name);
@@ -2374,7 +2380,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':');
$mount = $volume->after(':');
if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id";
$name = $name . "-pr-$pull_request_id";
}
$volume = str("$name:$mount");
}
@@ -2385,7 +2391,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$read_only = data_get($volume, 'read_only');
if ($source && $target) {
if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
$dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if (str($source, '.')) {
$source = str($source)->replaceFirst('.', $dir);
}
@@ -2393,23 +2399,23 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = str($source)->replaceFirst('~', $dir);
}
if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id";
$source = $source . "-pr-$pull_request_id";
}
if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro');
data_set($volume, 'source', $source . ':' . $target . ':ro');
} else {
data_set($volume, 'source', $source.':'.$target);
data_set($volume, 'source', $source . ':' . $target);
}
} else {
if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id";
$source = $source . "-pr-$pull_request_id";
}
if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro');
data_set($volume, 'source', $source . ':' . $target . ':ro');
} else {
data_set($volume, 'source', $source.':'.$target);
data_set($volume, 'source', $source . ':' . $target);
}
if (! str($source)->startsWith('/')) {
if (!str($source)->startsWith('/')) {
if ($topLevelVolumes->has($source)) {
$v = $topLevelVolumes->get($source);
if (data_get($v, 'driver_opts.type') === 'cifs') {
@@ -2442,11 +2448,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
if (is_string($volume)) {
$volume = str($volume);
if ($volume->contains(':') && ! $volume->startsWith('/')) {
if ($volume->contains(':') && !$volume->startsWith('/')) {
$name = $volume->before(':');
$mount = $volume->after(':');
if ($name->startsWith('.') || $name->startsWith('~')) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
$dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if ($name->startsWith('.')) {
$name = $name->replaceFirst('.', $dir);
}
@@ -2454,13 +2460,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir);
}
if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id";
$name = $name . "-pr-$pull_request_id";
}
$volume = str("$name:$mount");
} else {
if ($pull_request_id !== 0) {
$uuid = $resource->uuid;
$name = $uuid."-$name-pr-$pull_request_id";
$name = $uuid . "-$name-pr-$pull_request_id";
$volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name);
@@ -2479,7 +2485,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} else {
$uuid = $resource->uuid;
$name = str($uuid."-$name");
$name = str($uuid . "-$name");
$volume = str("$name:$mount");
if ($topLevelVolumes->has($name->value())) {
$v = $topLevelVolumes->get($name->value());
@@ -2502,7 +2508,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':');
$mount = $volume->after(':');
if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id";
$name = $name . "-pr-$pull_request_id";
}
$volume = str("$name:$mount");
}
@@ -2514,7 +2520,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($source && $target) {
$uuid = $resource->uuid;
if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
$dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if (str($source, '.')) {
$source = str($source)->replaceFirst('.', $dir);
}
@@ -2522,22 +2528,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = str($source)->replaceFirst('~', $dir);
}
if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro');
data_set($volume, 'source', $source . ':' . $target . ':ro');
} else {
data_set($volume, 'source', $source.':'.$target);
data_set($volume, 'source', $source . ':' . $target);
}
} else {
if ($pull_request_id === 0) {
$source = $uuid."-$source";
$source = $uuid . "-$source";
} else {
$source = $uuid."-$source-pr-$pull_request_id";
$source = $uuid . "-$source-pr-$pull_request_id";
}
if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro');
data_set($volume, 'source', $source . ':' . $target . ':ro');
} else {
data_set($volume, 'source', $source.':'.$target);
data_set($volume, 'source', $source . ':' . $target);
}
if (! str($source)->startsWith('/')) {
if (!str($source)->startsWith('/')) {
if ($topLevelVolumes->has($source)) {
$v = $topLevelVolumes->get($source);
if (data_get($v, 'driver_opts.type') === 'cifs') {
@@ -2570,7 +2576,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
$serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
return $dependency."-pr-$pull_request_id";
return $dependency . "-pr-$pull_request_id";
});
data_set($service, 'depends_on', $serviceDependencies->toArray());
}
@@ -2592,7 +2598,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
@@ -2620,7 +2626,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (! $definedNetworkExists) {
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
if ($pull_request_id !== 0) {
$topLevelNetworks->put($network, [
@@ -2738,12 +2744,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'application_id' => $resource->id,
])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if (! is_null($command)) {
if (!is_null($command)) {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($server, $containerName);
} else {
$fqdn = generateFqdn($server, Str::lower($forService).'-'.$resource->uuid);
$fqdn = generateFqdn($server, Str::lower($forService) . '-' . $resource->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
@@ -2764,7 +2770,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} else {
$generatedValue = generateEnvValue($command);
if (! $foundEnv) {
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
@@ -2936,7 +2942,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (! data_get($service, 'restart')) {
if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE);
}
data_set($service, 'container_name', $containerName);
@@ -2947,7 +2953,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
});
if ($pull_request_id !== 0) {
$services->each(function ($service, $serviceName) use ($pull_request_id, $services) {
$services[$serviceName."-pr-$pull_request_id"] = $service;
$services[$serviceName . "-pr-$pull_request_id"] = $service;
data_forget($services, $serviceName);
});
}
@@ -2975,7 +2981,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$uuid = data_get($resource, 'uuid');
$compose = data_get($resource, 'docker_compose_raw');
if (! $compose) {
if (!$compose) {
return collect([]);
}
@@ -3368,29 +3374,29 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$isDirectory = data_get($foundConfig, 'is_directory');
} else {
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) {
$isDirectory = true;
}
}
}
if ($type->value() === 'bind') {
if ($source->value() === '/var/run/docker.sock') {
$volume = $source->value().':'.$target->value();
$volume = $source->value() . ':' . $target->value();
} elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') {
$volume = $source->value().':'.$target->value();
$volume = $source->value() . ':' . $target->value();
} else {
if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
$mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid);
} elseif ($isService) {
$mainDirectory = str(base_configuration_dir().'/services/'.$uuid);
$mainDirectory = str(base_configuration_dir() . '/services/' . $uuid);
}
} else {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
$mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid);
}
$source = replaceLocalSource($source, $mainDirectory);
if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId";
$source = $source . "-pr-$pullRequestId";
}
LocalFileVolume::updateOrCreate(
[
@@ -3410,12 +3416,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if (isDev()) {
if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid);
} elseif ($isService) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid);
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/' . $uuid);
}
} else {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid);
}
}
$volume = "$source:$target";
@@ -3482,7 +3488,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$depends_on = $newDependsOn;
}
}
if (! $use_network_mode) {
if (!$use_network_mode) {
if ($topLevel->get('networks')?->count() > 0) {
foreach ($topLevel->get('networks') as $networkName => $network) {
if ($networkName === 'default') {
@@ -3495,7 +3501,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$networkExists = $networks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
if (!$networkExists) {
$networks->put($networkName, null);
}
}
@@ -3503,7 +3509,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) {
return $value == $baseNetwork;
});
if (! $baseNetworkExists) {
if (!$baseNetworkExists) {
foreach ($baseNetwork as $network) {
$topLevel->get('networks')->put($network, [
'name' => $network,
@@ -3535,7 +3541,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$networks_temp = collect();
if (! $use_network_mode) {
if (!$use_network_mode) {
foreach ($networks as $key => $network) {
if (gettype($network) === 'string') {
// networks:
@@ -3566,7 +3572,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$normalEnvironments = $environment->diffKeys($allMagicEnvironments);
$normalEnvironments = $normalEnvironments->filter(function ($value, $key) {
return ! str($value)->startsWith('SERVICE_');
return !str($value)->startsWith('SERVICE_');
});
foreach ($normalEnvironments as $key => $value) {
@@ -3586,7 +3592,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
continue;
}
if (! $value->startsWith('$')) {
if (!$value->startsWith('$')) {
continue;
}
if ($key->value() === $parsedValue->value()) {
@@ -3717,7 +3723,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
}
// Add COOLIFY_FQDN & COOLIFY_URL to environment
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
$coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(','));
$urls = $fqdns->map(function ($fqdn) {
@@ -3729,7 +3735,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
return !str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
if (str($value)->isEmpty()) {
@@ -3756,7 +3762,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
});
}
}
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
$uuid = $resource->uuid;
@@ -3849,7 +3855,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'restart' => $restart->value(),
'labels' => $serviceLabels,
]);
if (! $use_network_mode) {
if (!$use_network_mode) {
$payload['networks'] = $networks_temp;
}
if ($ports->count() > 0) {
@@ -3909,7 +3915,7 @@ function isAssociativeArray($array)
$array = $array->toArray();
}
if (! is_array($array)) {
if (!is_array($array)) {
throw new \InvalidArgumentException('Input must be an array or a Collection.');
}
@@ -4022,7 +4028,7 @@ function instanceSettings()
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
{
$server = Server::find($server_id)->where('team_id', $team_id)->first();
if (! $server) {
if (!$server) {
return;
}
$uuid = new Cuid2;
@@ -4049,7 +4055,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
function loggy($message = null, array $context = [])
{
if (! isDev()) {
if (!isDev()) {
return;
}
if (function_exists('ray') && config('app.debug')) {
@@ -4084,7 +4090,7 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
$limiterKey,
$maxAttempts = 0,
function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
isDev() && loggy('Rate limit not reached for '.$limiterKey);
isDev() && loggy('Rate limit not reached for ' . $limiterKey);
$rateLimited = false;
if ($callbackOnSuccess) {
@@ -4093,8 +4099,8 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
},
$decaySeconds,
);
if (! $executed) {
isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.');
if (!$executed) {
isDev() && loggy('Rate limit reached for ' . $limiterKey . '. Rate limiter will be disabled for ' . $decaySeconds . ' seconds.');
$rateLimited = true;
}
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->boolean('slack_enabled')->default(false);
$table->string('slack_webhook_url')->nullable();
$table->boolean('slack_notifications_test')->default(true);
$table->boolean('slack_notifications_deployments')->default(true);
$table->boolean('slack_notifications_status_changes')->default(true);
$table->boolean('slack_notifications_database_backups')->default(true);
$table->boolean('slack_notifications_scheduled_tasks')->default(true);
$table->boolean('slack_notifications_server_disk_usage')->default(true);
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn([
'slack_enabled',
'slack_webhook_url',
'slack_notifications_test',
'slack_notifications_deployments',
'slack_notifications_status_changes',
'slack_notifications_database_backups',
'slack_notifications_scheduled_tasks',
'slack_notifications_server_disk_usage',
]);
});
}
};
+11
View File
@@ -0,0 +1,11 @@
<svg width="346" height="434" xmlns="http://www.w3.org/2000/svg">
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="436" width="348" y="-1" x="-1"/>
</g>
<g>
<title>Layer 1</title>
<path id="svg_1" d="m221.227,397.119l0,-24.919l8.552,-26.577c59.868,-59.323 106.907,-114.847 115.221,-131.932l-10.215,3.56s9.5,-11.864 8.552,-18.51c-7.6,-116.273 -73.17,-176.862 -91.226,-191.732c26.607,31.955 19.326,42.4 19.326,42.4c-11.723,-21.833 -35.797,-43.032 -40.549,-48.409l-5.7,33.853c22.806,13.289 16.79,20.882 16.79,20.882c-5.387,-7.594 -19.957,-14.235 -19.957,-14.235l-1.586,9.491c6.973,3.165 38.327,22.149 38.565,70.872c0,59.323 -33.735,74.985 -39.674,77.357c-19.718,-11.865 -41.1,-11.627 -46.326,-12.1c-5.226,0.474 -26.608,0.236 -46.324,12.1c-5.941,-2.376 -39.676,-18.038 -39.676,-77.361c0.238,-48.723 31.6,-67.707 38.564,-70.872l-1.581,-9.487s-14.574,6.644 -19.955,14.238c0,0 -6.022,-7.593 16.786,-20.882l-5.702,-33.856c-4.752,5.378 -28.824,26.577 -40.543,48.407c0,0 -7.288,-10.443 19.32,-42.4c-18.054,14.87 -83.625,75.459 -91.226,191.732c-0.95,6.646 8.554,18.51 8.554,18.51l-10.217,-3.558c8.316,17.085 55.355,72.612 115.221,131.935l8.554,26.577l0,24.916l-7.6,-26.577l-17.345,-28.475l0,28.95c7.127,20.169 34.921,60.983 34.921,60.983l0,-84.475l-106.431,-116.037s66.52,54.1 85.526,73.56l9.978,-10.2c-12.355,-8.78 -66.045,-62.646 -70.8,-110.1c40.388,85.426 79.111,96.1 107.62,129.325l0,8.127l24.707,0l0,-8.131c28.508,-33.221 67.233,-43.9 107.619,-129.325c-4.75,47.459 -58.442,101.325 -70.8,110.1l9.976,10.2c19.006,-19.458 85.526,-73.56 85.526,-73.56l-106.423,116.041l0,84.475s27.8,-40.815 34.923,-60.984l0,-28.95l-17.343,28.475l-7.602,26.578zm-48.227,-102.036c-17.818,-11.865 -63.668,-39.629 -82.672,-83.053a314.117,314.117 0 0 0 34.447,34.882s-1.189,-30.61 1.186,-37.255c4.276,36.781 13.068,52.442 47.039,71.9c33.973,-19.458 42.763,-35.119 47.039,-71.9c2.375,6.645 1.188,37.255 1.188,37.255a314.117,314.117 0 0 0 34.447,-34.882c-19.006,43.424 -64.856,71.188 -82.674,83.053z" class="cls-1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg title="Penpot logo" alt="Penpot logo" height="500" viewBox="0 0 1512.0058 499.99999" width="1512.0059" xmlns="http://www.w3.org/2000/svg">
<path d="M92.418 16.013l-49.21 69.31v39.179L.386 144.87 0 144.69v255.605l166.955 78.877 10.203 4.815 10.202-4.815 166.955-78.877V144.689l-.314.149-42.835-20.376V85.324l-1.498-2.108-47.714-67.203-49.212 69.31v.07l-35.81-50.433-35.53 50.044-1.27-1.788zm8.707 40.26l19.033 26.807h-55.48l18.813-26.492zm169.533 0l19.036 26.807h-55.48l18.808-26.492zm-85.02 18.95l19.034 26.803h-55.474l18.807-26.491zM59.419 96.589H85.73v88.601l-26.313-12.432zm39.822 0h26.181v107.35L99.24 191.57zm129.716 0h26.31v94.89l-26.31 12.43zm39.817 0h26.18v76.14l-26.18 12.367zm-124.839 18.946h26.313v109.579l-26.313-12.432zm39.822 0h26.18v97.36l-26.18 12.37zm1250.79 9.673l-.13 49.326-26.45-.18v20.098h26.528v105.964c0 7.718.601 14.876 1.823 21.473l.012.057.013.06c1.498 6.739 4.03 12.614 7.641 17.517 3.643 4.943 8.518 8.743 14.402 11.31 6.002 2.854 13.305 4.188 21.874 4.188 3.906 0 8.377-.638 13.502-1.858a122.683 122.683 0 0015.265-4.723l2.98-1.137-6.428-19.274-3.37 1.556c-6.548 2.838-12.965 4.96-19.827 4.998-5.528 0-9.873-.794-13.056-2.164-3.037-1.644-5.384-3.704-7.166-6.262-1.86-2.908-3.227-6.253-4.091-10.096-.666-4.251-1.014-8.99-1.014-14.228V194.452h49.163V174.37h-49.163v-49.163zM311.162 144.655l23.167 9.471-23.167 10.944zm-267.954.034v20.413l-23.162-10.943zm1085.59 25.431l.003.003v.002c-11.151 0-21.974 2.746-32.31 8.16l-.014.01-.015.007c-8.35 4.48-15.318 9.158-22.644 14.686l-.132-18.617h-21.77v253.156h22.204v-90.49c7.276 4.963 14.464 9.266 21.502 12.394l.035.015.037.017c8.831 3.68 17.53 5.558 26.036 5.558 10.35 0 20.273-2.106 29.649-6.301l.017-.008.02-.01c9.613-4.436 18.024-10.749 25.151-18.86l-.007.003c7.38-8.365 13.133-18.423 17.264-30.088 4.156-11.735 6.202-24.97 6.202-39.66 0-26.873-5.764-48.61-17.599-64.892l-.005-.008c-11.928-16.645-30.22-25.077-53.623-25.077zm-537.717.003v.002c-11.152 0-21.974 2.746-32.31 8.16l-.014.01-.015.007c-8.35 4.48-15.318 9.16-22.645 14.689l-.13-18.62h-21.77V427.53H536.4v-90.491c7.276 4.963 14.463 9.264 21.502 12.392l.035.017.037.018c8.831 3.68 17.53 5.558 26.036 5.558 10.35 0 20.273-2.107 29.649-6.301l.017-.01.02-.008c9.612-4.437 18.024-10.75 25.151-18.86l-.007.003c7.38-8.365 13.133-18.423 17.264-30.088 4.156-11.735 6.202-24.97 6.202-39.661 0-26.873-5.764-48.61-17.599-64.892l-.005-.007c-11.928-16.645-30.218-25.077-53.621-25.077zm361.519 0v.002c-12.33 0-23.506 3.12-33.236 9.348-7.945 4.891-15.644 11.122-23.19 18.189l.156-23.291h-21.326v176.407h22.208V222.78c9.853-9.975 18.726-19.422 26.387-24.238l.027-.02.028-.017c7.73-5.08 16.216-7.59 25.763-7.59 13.216 0 21.738 3.903 26.954 11.623l.03.042.032.044c5.472 7.577 8.565 20.24 8.565 38.022v110.13h22.206V238.523c0-22.576-4.137-39.704-13.078-51.2-8.959-11.517-23.178-17.2-41.526-17.2zm-182.585.002c-9.918 0-19.62 2.244-28.976 6.67-.005.003-.009.006-.015.008-9.149 4.207-17.416 10.392-24.752 18.461l-.002.003c-7.152 7.891-12.895 17.58-17.262 28.983l-.013.03-.01.03c-4.16 11.5-6.212 24.387-6.212 38.618 0 14.23 2.052 27.12 6.212 38.62l.012.04.018.04c4.374 11.178 10.243 20.86 17.611 28.965l.02.018.015.02c7.376 7.868 16.035 13.931 25.881 18.116l.013.005.015.005c10.073 4.177 20.787 6.264 32.042 6.264 12.795 0 23.485-1.786 32.08-5.57l.017-.01c8.357-3.583 16.02-7.414 22.988-11.499l2.747-1.607-9.665-18.253-3.106 2.301c-6.11 4.526-12.137 7.34-19.392 10.064-6.888 2.44-15.384 3.69-24.255 3.69-9.242 0-17.45-.877-24.747-4.185-7.372-3.58-13.694-8.354-19.05-14.367-5.13-6.257-9.176-13.645-12.12-22.248-.001-.003-.001-.006-.003-.008-2.61-7.84-3.984-16.33-4.463-25.307h122.424c0-3.46.332-6.785.344-9.69v-6.365c0-25.505-5.917-45.711-18.114-60.149-12.172-14.407-29.258-21.69-50.282-21.69zm538.136 0c-10.576 0-20.72 2.103-30.326 6.29-9.609 4.188-18.136 10.248-25.5 18.103l-.017.02-.023.02c-7.163 7.904-12.907 17.72-17.272 29.36l-.007.017-.008.02c-4.162 11.507-6.21 24.512-6.21 38.972 0 14.461 2.048 27.468 6.21 38.975l.015.04.017.04c4.37 11.166 10.113 20.73 17.253 28.609l.02.017.02.022c7.363 7.855 15.888 13.913 25.497 18.102 9.606 4.187 19.754 6.289 30.33 6.289 10.577 0 20.723-2.102 30.328-6.29 9.836-4.184 18.377-10.247 25.518-18.12 7.392-7.89 13.153-17.481 17.292-28.674 4.407-11.517 6.579-24.54 6.579-39.01 0-14.464-2.17-27.48-6.574-38.995-4.133-11.652-9.89-21.487-17.3-29.396-7.14-7.872-15.68-13.938-25.515-18.122-9.604-4.187-19.75-6.289-30.325-6.289zm-1287.748 6.77l146.552 69.231v210.475l-146.552-69.23zm313.507 0V387.37l-146.55 69.231V246.126zm436.454 13.313h.003c14.542 0 25.502 4.934 33.87 15.063l.007.015.01.015c7.776 9.208 12.013 23.687 12.474 43.342H715.896c1.054-8.32 2.805-15.96 5.546-22.623 3.402-7.696 7.557-14.188 12.45-19.53 5.14-5.358 10.76-9.311 16.917-11.95l.044-.022.045-.02c6.177-2.867 12.622-4.29 19.467-4.29zm-181.757.352v.003c9.235 0 16.894 1.793 23.152 5.246l.04.02.039.022c6.338 3.278 11.401 7.906 15.352 14.052l.018.025.014.022c3.972 5.957 6.894 13.206 8.71 21.83l.007.042.013.04c2.056 8.68 3.094 18.089 3.094 28.237 0 11.26-1.494 21.522-4.444 30.825l-.01.025-.008.025c-2.732 9.108-6.57 16.867-11.488 23.35-4.911 6.474-10.757 11.427-17.624 14.971-6.843 3.532-14.175 5.288-22.171 5.288-6.048Line truncated
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -15,6 +15,10 @@
href="{{ route('notifications.discord') }}">
<button>Discord</button>
</a>
<a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}"
href="{{ route('notifications.slack') }}">
<button>Slack</button>
</a>
</nav>
</div>
</div>
</div>
@@ -0,0 +1,43 @@
<div>
<x-slot:title>
Notifications | Coolify
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<h2>Slack</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($slackEnabled)
<x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
</x-forms.button>
@endif
</div>
<div class="w-32">
<x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
</div>
<x-forms.input type="password"
helper="Generate a webhook in Slack.<br>Example: https://hooks.slack.com/services/...." required
id="slackWebhookUrl" label="Webhook" />
</form>
@if ($slackEnabled)
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsTest" label="Test" />
@endif
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsStatusChanges"
label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsDeployments"
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsDatabaseBackups" label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsScheduledTasks"
label="Scheduled Tasks Status" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsServerDiskUsage"
label="Server Disk Usage" />
</div>
@endif
</div>
@@ -27,6 +27,13 @@
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
id="server.settings.generate_exact_labels"
label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave />
<x-forms.checkbox instantSave="instantSaveRedirect" id="redirect_enabled"
label="Override default request handler"
helper="Requests to unknown hosts or stopped services will recieve a 503 response or be redirected to the URL you set below (need to enable this first)." />
@if ($redirect_enabled)
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url"
label="Redirect to (optional)" />
@endif
</div>
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
<h4>Traefik</h4>
@@ -40,8 +47,6 @@
configurations.
</div>
@endif
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url" label="Default Redirect 404"
helper="All urls that has no service available will be redirected to this domain." />
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
<x-loading text="Loading proxy configuration..." />
</div>
@@ -29,7 +29,8 @@
@if (str_replace('|', '.', $fileName) === 'coolify.yaml' ||
str_replace('|', '.', $fileName) === 'Caddyfile' ||
str_replace('|', '.', $fileName) === 'coolify.caddy' ||
str_replace('|', '.', $fileName) === 'default_redirect_404.caddy')
str_replace('|', '.', $fileName) === 'default_redirect_503.yaml' ||
str_replace('|', '.', $fileName) === 'default_redirect_503.caddy')
<div>
<h3 class="dark:text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
</div>
+5 -3
View File
@@ -13,6 +13,7 @@ use App\Livewire\ForcePasswordReset;
use App\Livewire\Notifications\Discord as NotificationDiscord;
use App\Livewire\Notifications\Email as NotificationEmail;
use App\Livewire\Notifications\Telegram as NotificationTelegram;
use App\Livewire\Notifications\Slack as NotificationSlack;
use App\Livewire\Profile\Index as ProfileIndex;
use App\Livewire\Project\Application\Configuration as ApplicationConfiguration;
use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex;
@@ -132,6 +133,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/email', NotificationEmail::class)->name('notifications.email');
Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram');
Route::get('/discord', NotificationDiscord::class)->name('notifications.discord');
Route::get('/slack', NotificationSlack::class)->name('notifications.slack');
});
Route::prefix('storages')->group(function () {
@@ -285,7 +287,7 @@ Route::middleware(['auth'])->group(function () {
'privateKey' => $privateKeyLocation,
'root' => '/',
]);
if (! $disk->exists($filename)) {
if (!$disk->exists($filename)) {
return response()->json(['message' => 'Backup not found.'], 404);
}
@@ -297,7 +299,7 @@ Route::middleware(['auth'])->group(function () {
if ($stream === false || is_null($stream)) {
abort(500, 'Failed to open stream for the requested file.');
}
while (! feof($stream)) {
while (!feof($stream)) {
echo fread($stream, 2048);
flush();
}
@@ -305,7 +307,7 @@ Route::middleware(['auth'])->group(function () {
fclose($stream);
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.basename($filename).'"',
'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"',
]);
} catch (\Throwable $e) {
return response()->json(['message' => $e->getMessage()], 500);
+3 -3
View File
@@ -30,7 +30,7 @@ WARNING_SPACE=false
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
WARNING_SPACE=true
cat << 'EOF'
cat << EOF
WARNING: Insufficient total disk space!
Total disk space: ${TOTAL_SPACE}GB
@@ -41,7 +41,7 @@ EOF
fi
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
cat << 'EOF'
cat << EOF
WARNING: Insufficient available disk space!
Available disk space: ${AVAILABLE_SPACE}GB
@@ -49,7 +49,7 @@ Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
==================
EOF
WARNING_SPACE=true
WARNING_SPACE=true
fi
if [ "$WARNING_SPACE" = true ]; then
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://github.com/linuxserver/Heimdall
# slogan: Heimdall is a dashboard for managing and organizing your server applications.
# tags: dashboard, server, applications, interface
# logo: svgs/heimdall.svg
services:
heimdall:
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://invoiceninja.github.io/selfhost.html
# slogan: The leading open-source invoicing platform
# tags: invoicing, billing, accounting, finance, self-hosted
# logo: svgs/invoiceninja.png
# port: 9000
services:
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://kuzzle.io
# slogan: Kuzzle is a generic backend offering the basic building blocks common to every application.
# tags: backend, api, realtime, websocket, mqtt, rest, sdk, iot, geofencing, low-code
# logo: svgs/kuzzle.png
# port: 7512
services:
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://pairdrop.net/
# slogan: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.
# tags: file, sharing, collaboration, teamwork
# logo: svgs/pairdrop.png
# port: 3000
services:
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://help.penpot.app/technical-guide/getting-started/#install-with-docker
# slogan: Penpot is the first Open Source design and prototyping platform for product teams.
# tags: penpot,design,prototyping,figma,open,source
# logo: svgs/penpot.svg
services:
frontend:
+2 -3
View File
@@ -5,7 +5,6 @@
# port: 3000
x-common-env: &common-env
PORT: 3030
REMIX_APP_PORT: 3000
NODE_ENV: production
RUNTIME_PLATFORM: docker-compose
@@ -118,7 +117,7 @@ services:
environment:
<<: *common-env
PLATFORM_HOST: trigger
PLATFORM_WS_PORT: 3030
PLATFORM_WS_PORT: 3000
SECURE_CONNECTION: "false"
PLATFORM_SECRET: $PROVIDER_SECRET
coordinator:
@@ -133,7 +132,7 @@ services:
environment:
<<: *common-env
PLATFORM_HOST: trigger
PLATFORM_WS_PORT: 3030
PLATFORM_WS_PORT: 3000
SECURE_CONNECTION: "false"
PLATFORM_SECRET: $COORDINATOR_SECRET
healthcheck:
+1
View File
@@ -1,6 +1,7 @@
# documentation: https://github.com/benbusby/whoogle-search?tab=readme-ov-file
# slogan: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.
# tags: privacy, search engine
# logo: svgs/whoogle.png
# port: 5000
services:
+8 -8
View File
@@ -1125,7 +1125,7 @@
"applications",
"interface"
],
"logo": "svgs/default.webp",
"logo": "svgs/heimdall.svg",
"minversion": "0.0.0"
},
"heyform": {
@@ -1251,7 +1251,7 @@
"finance",
"self-hosted"
],
"logo": "svgs/default.webp",
"logo": "svgs/invoiceninja.png",
"minversion": "0.0.0",
"port": "9000"
},
@@ -1417,7 +1417,7 @@
"geofencing",
"low-code"
],
"logo": "svgs/default.webp",
"logo": "svgs/kuzzle.png",
"minversion": "0.0.0",
"port": "7512"
},
@@ -2112,7 +2112,7 @@
"collaboration",
"teamwork"
],
"logo": "svgs/default.webp",
"logo": "svgs/pairdrop.png",
"minversion": "0.0.0",
"port": "3000"
},
@@ -2137,7 +2137,7 @@
"open",
"source"
],
"logo": "svgs/default.webp",
"logo": "svgs/penpot.svg",
"minversion": "0.0.0"
},
"phpmyadmin": {
@@ -2245,7 +2245,7 @@
"postiz": {
"documentation": "https://docs.postiz.com?utm_source=coolify.io",
"slogan": "Open source social media scheduling tool.",
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfRlFETl9QT1NUSVp9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ1hfQVBJX0tFWT0ke1NFUlZJQ0VfWF9BUEl9JwogICAgICAtICdYX0FQSV9TRUNSRVQ9JHtTRVJWSUNFX1hfU0VDUkVUfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX0lEPSR7U0VSVklDRV9MSU5LRURJTl9JRH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0xJTktFRElOX1NFQ1JFVH0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1JFRERJVF9BUEl9JwogICAgICAtICdSRURESVRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUkVERElUX1NFQ1JFVH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfSUQ9JHtTRVJWSUNFX0dJVEhVQl9JRH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9HSVRIVUJfU0VDUkVUfScKICAgICAgLSAnVEhSRUFEU19BUFBfSUQ9JHtTRVJWSUNFX1RIUkVBRFNfSUR9JwogICAgICAtICdUSFJFQURTX0FQUF9TRUNSRVQ9JHtTRVJWSUNFX1RIUkVBRFNfU0VDUkVUfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX0lEPSR7U0VSVklDRV9GQUNFQk9PS19JRH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9TRUNSRVQ9JHtTRVJWSUNFX0ZBQ0VCT09LX1NFQ1JFVH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX0lEPSR7U0VSVklDRV9ZT1VUVUJFX0lEfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9ZT1VUVUJFX1NFQ1JFVH0nCiAgICAgIC0gJ1RJS1RPS19DTElFTlRfSUQ9JHtTRVJWSUNFX1RJS1RPS19JRH0nCiAgICAgIC0gJ1RJS1RPS19DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9USUtUT0tfU0VDUkVUfScKICAgICAgLSAnUElOVEVSRVNUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUElOVEVSRVNUX0lEfScKICAgICAgLSAnUElOVEVSRVNUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9TRUNSRVR9JwogICAgICAtICdEUklCQkJMRV9DTElFTlRfSUQ9JHtTRVJWSUNFX0RSSUJCTEVfSUR9JwogICAgICAtICdEUklCQkJMRV9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9EUklCQkxFX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX0lEPSR7U0VSVklDRV9ESVNDT1JEX0lEfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9ESVNDT1JEX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfQk9UX1RPS0VOX0lEPSR7U0VSVklDRV9ESVNDT1JEX1RPS0VOfScKICAgICAgLSAnU0xBQ0tfSUQ9JHtTRVJWSUNFX1NMQUNLX0lEfScKICAgICAgLSAnU0xBQ0tfU0VDUkVUPSR7U0VSVklDRV9TTEFDS19TRUNSRVR9JwogICAgICAtICdTTEFDS19TSUdOSU5HX1NFQ1JFVD0ke1NMQUNLX1NJR05JTkdfU0VDUkVUfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX0lEPSR7U0VSVklDRV9NQVNUT0RPTl9JRH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX01BU1RPRE9OX1NFQ1JFVH0nCiAgICAgIC0gJ0JFRUhJSVZFX0FQSV9LRVk9JHtTRVJWSUNFX0JFRUhJSVZFX0tFWX0nCiAgICAgIC0gJ0JFRUhJSVZFX1BVQkxJQ0FUSU9OX0lEPSR7U0VSVklDRV9CRUVISUlWRV9QVUJJRH0nCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7U0VSVklDRV9PUEVOQUlfS0VZfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQ09SRF9TVVBQT1JUPSR7TkVYVF9QVUJMSUNfRElTQ09SRF9TVVBQT1JUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfUE9MT1ROTz0ke05FWFRfUFVCTElDX1BPTE9UTk99JwogICAgICAtIElTX0dFTkVSQUw9dHJ1ZQogICAgICAtICdOWF9BRERfUExVR0lOUz0ke05YX0FERF9QTFVHSU5TOi1mYWxzZX0nCiAgICAgIC0gJ0ZFRV9BTU9VTlQ9JHtGRUVfQU1PVU5UOi0wLjA1fScKICAgICAgLSAnU1RSSVBFX1BVQkxJU0hBQkxFX0tFWT0ke1NUUklQRV9QVUJMSVNIQUJMRV9LRVl9JwogICAgICAtICdTVFJJUEVfU0VDUkVUX0tFWT0ke1NUUklQRV9TRUNSRVRfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZPSR7U1RSSVBFX1NJR05JTkdfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1Q9JHtTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfY29uZmlnOi9jb25maWcvJwogICAgICAtICdwb3N0aXpfdXBsb2FkczovdXBsb2Fkcy8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1MDAwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRLine truncated
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfRlFETl9QT1NUSVp9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1BST1ZJREVSPSR7RU1BSUxfUFJPVklERVJ9JwogICAgICAtICdYX0FQSV9LRVk9JHtTRVJWSUNFX1hfQVBJfScKICAgICAgLSAnWF9BUElfU0VDUkVUPSR7U0VSVklDRV9YX1NFQ1JFVH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTElOS0VESU5fSUR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9MSU5LRURJTl9TRUNSRVR9JwogICAgICAtICdSRURESVRfQ0xJRU5UX0lEPSR7U0VSVklDRV9SRURESVRfQVBJfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1JFRERJVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7U0VSVklDRV9HSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfR0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX0lEPSR7U0VSVklDRV9USFJFQURTX0lEfScKICAgICAgLSAnVEhSRUFEU19BUFBfU0VDUkVUPSR7U0VSVklDRV9USFJFQURTX1NFQ1JFVH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9JRD0ke1NFUlZJQ0VfRkFDRUJPT0tfSUR9JwogICAgICAtICdGQUNFQk9PS19BUFBfU0VDUkVUPSR7U0VSVklDRV9GQUNFQk9PS19TRUNSRVR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfWU9VVFVCRV9JRH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfWU9VVFVCRV9TRUNSRVR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX0lEPSR7U0VSVklDRV9USUtUT0tfSUR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfVElLVE9LX1NFQ1JFVH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9JRH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9QSU5URVJFU1RfU0VDUkVUfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX0lEPSR7U0VSVklDRV9EUklCQkxFX0lEfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRFJJQkJMRV9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRElTQ09SRF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRElTQ09SRF9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0JPVF9UT0tFTl9JRD0ke1NFUlZJQ0VfRElTQ09SRF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0lEPSR7U0VSVklDRV9TTEFDS19JRH0nCiAgICAgIC0gJ1NMQUNLX1NFQ1JFVD0ke1NFUlZJQ0VfU0xBQ0tfU0VDUkVUfScKICAgICAgLSAnU0xBQ0tfU0lHTklOR19TRUNSRVQ9JHtTTEFDS19TSUdOSU5HX1NFQ1JFVH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTUFTVE9ET05fSUR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9NQVNUT0RPTl9TRUNSRVR9JwogICAgICAtICdCRUVISUlWRV9BUElfS0VZPSR7U0VSVklDRV9CRUVISUlWRV9LRVl9JwogICAgICAtICdCRUVISUlWRV9QVUJMSUNBVElPTl9JRD0ke1NFUlZJQ0VfQkVFSElJVkVfUFVCSUR9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0tFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVD0ke05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1BPTE9UTk89JHtORVhUX1BVQkxJQ19QT0xPVE5PfScKICAgICAgLSBJU19HRU5FUkFMPXRydWUKICAgICAgLSAnTlhfQUREX1BMVUdJTlM9JHtOWF9BRERfUExVR0lOUzotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8Line truncated
"tags": [
"post everywhere",
"social media",
@@ -2696,7 +2696,7 @@
"trigger": {
"documentation": "https://trigger.dev?utm_source=coolify.io",
"slogan": "The open source Background Jobs framework for TypeScript",
"compose": "eC1jb21tb24tZW52OgogIFBPUlQ6IDMwMzAKICBSRU1JWF9BUFBfUE9SVDogMzAwMAogIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICBWM19FTkFCTEVEOiB0cnVlCiAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgTUFHSUNfTElOS19TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X01BR0lDCiAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogIFBST1ZJREVSX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfUFJPVklERVIKICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgREFUQUJBU0VfSE9TVDogJ3Bvc3RncmVzcWw6NTQzMicKICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUnCiAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICBSRURJU19IT1NUOiByZWRpcwogIFJFRElTX1BPUlQ6IDYzNzkKICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICBDT09SRElOQVRPUl9QT1JUOiA5MDIwCiAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogIEFETUlOX0VNQUlMUzogJycKICBERUZBVUxUX09SR19FWEVDVVRJT05fQ09OQ1VSUkVOQ1lfTElNSVQ6IDMwMAogIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogIERFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0U6IHRyaWdnZXIKICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICBBVVRIX0dJVEhVQl9DTElFTlRfSUQ6ICcke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICBGUk9NX0VNQUlMOiAnJHtGUk9NX0VNQUlMfScKICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICBBUFBfT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgRUxFQ1RSSUNfT1JJR0lOOiAnaHR0cDovL2VsZWN0cmljOjMwMDAnCnNlcnZpY2VzOgogIHRyaWdnZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vdHJpZ2dlcmRvdGRldi90cmlnZ2VyLmRldjp2MycKICAgIGVudmlyb25tZW50OgogICAgICBTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwOiAnJwogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9Line truncated
"compose": "eC1jb21tb24tZW52OgogIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgTk9ERV9FTlY6IHByb2R1Y3Rpb24KICBSVU5USU1FX1BMQVRGT1JNOiBkb2NrZXItY29tcG9zZQogIFYzX0VOQUJMRUQ6IHRydWUKICBJTlRFUk5BTF9PVEVMX1RSQUNFX0RJU0FCTEVEOiAxCiAgSU5URVJOQUxfT1RFTF9UUkFDRV9MT0dHSU5HX0VOQUJMRUQ6IDAKICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgUE9TVEdSRVNfREI6ICcke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICBTRVNTSU9OX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogIEVOQ1JZUFRJT05fS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogIENPT1JESU5BVE9SX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfQ09PUkRJTkFUT1IKICBEQVRBQkFTRV9IT1NUOiAncG9zdGdyZXNxbDo1NDMyJwogIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICBESVJFQ1RfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogIFJFRElTX0hPU1Q6IHJlZGlzCiAgUkVESVNfUE9SVDogNjM3OQogIFJFRElTX1RMU19ESVNBQkxFRDogdHJ1ZQogIENPT1JESU5BVE9SX0hPU1Q6IDEyNy4wLjAuMQogIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICBXSElURUxJU1RFRF9FTUFJTFM6ICcnCiAgQURNSU5fRU1BSUxTOiAnJwogIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgREVGQVVMVF9FTlZfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAxMDAKICBERVBMT1lfUkVHSVNUUllfSE9TVDogZG9ja2VyLmlvCiAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogIFJFR0lTVFJZX0hPU1Q6ICcke0RFUExPWV9SRUdJU1RSWV9IT1NUfScKICBSRUdJU1RSWV9OQU1FU1BBQ0U6ICcke0RFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0V9JwogIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICBBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICBSRVNFTkRfQVBJX0tFWTogJyR7UkVTRU5EX0FQSV9LRVl9JwogIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogIFJFUExZX1RPX0VNQUlMOiAnJHtSRVBMWV9UT19FTUFJTH0nCiAgTE9HSU5fT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgREVWX09URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAvb3RlbAogIE9URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJ2h0dHA6Ly90cmlnZ2VyOjMwNDAvb3RlbCcKICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKc2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OnYzJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDA6ICcnCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdLine truncated
"tags": [
"trigger.dev",
"background jobs",
@@ -2971,7 +2971,7 @@
"privacy",
"search engine"
],
"logo": "svgs/default.webp",
"logo": "svgs/whoogle.png",
"minversion": "0.0.0",
"port": "5000"
},