Merge branch 'next' into main

This commit is contained in:
Marvin von Rappard
2024-11-25 13:44:24 +01:00
committed by GitHub
25 changed files with 364 additions and 275 deletions

View File

@@ -31,7 +31,7 @@ class UpdateCoolify
} }
CleanupDocker::dispatch($this->server); CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify(); $this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version'); $this->currentVersion = config('constants.coolify.version');
if (! $manual_update) { if (! $manual_update) {
if (! $settings->is_auto_update_enabled) { if (! $settings->is_auto_update_enabled) {
return; return;

View File

@@ -200,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup() private function restore_coolify_db_backup()
{ {
if (version_compare('4.0.0-beta.179', config('version'), '<=')) { if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try { try {
$database = StandalonePostgresql::withTrashed()->find(0); $database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) { if ($database && $database->trashed()) {
@@ -228,7 +228,7 @@ class Init extends Command
private function send_alive_signal() private function send_alive_signal()
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('version'); $version = config('constants.coolify.version');
$settings = instanceSettings(); $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track'); $do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) { if ($do_not_track == true) {
@@ -264,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name() private function replace_slash_in_environment_name()
{ {
if (version_compare('4.0.0-beta.298', config('version'), '<=')) { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all(); $environments = Environment::all();
foreach ($environments as $environment) { foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) { if (str_contains($environment->name, '/')) {

View File

@@ -18,7 +18,7 @@ class DatabaseProxyStopped implements ShouldBroadcast
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId)) {
$teamId = Auth::user()->currentTeam()->id ?? null; $teamId = Auth::user()?->currentTeam()?->id ?? null;
} }
if (is_null($teamId)) { if (is_null($teamId)) {
throw new \Exception('Team id is null'); throw new \Exception('Team id is null');

View File

@@ -37,7 +37,7 @@ class OtherController extends Controller
)] )]
public function version(Request $request) public function version(Request $request)
{ {
return response(config('version')); return response(config('constants.coolify.version'));
} }
#[OA\Get( #[OA\Get(

View File

@@ -567,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Servers'], tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
requestBody: new OA\RequestBody( requestBody: new OA\RequestBody(
required: true, required: true,
description: 'Server updated.', description: 'Server updated.',
@@ -596,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType( new OA\MediaType(
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'array', ref: '#/components/schemas/Server'
items: new OA\Items(ref: '#/components/schemas/Server')
) )
), ),
]), ]),
@@ -679,7 +681,7 @@ class ServersController extends Controller
} }
return response()->json([ return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201); ])->setStatusCode(201);
} }

View File

@@ -463,7 +463,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables(); $this->save_environment_variables();
if (! is_null($this->env_filename)) { if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']); $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) { $services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename]; $service['env_file'] = [$this->env_filename];

View File

@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json(); $versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version'); $latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version'); $current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) { if (version_compare($latest_version, $current_version, '>')) {
// New version available // New version available

View File

@@ -67,6 +67,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void public function handle(): void
{ {
try { try {
$databasesToBackup = null;
$this->team = Team::find($this->backup->team_id); $this->team = Team::find($this->backup->team_id);
if (! $this->team) { if (! $this->team) {
$this->backup->delete(); $this->backup->delete();

View File

@@ -25,218 +25,222 @@ class StripeProcessJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
$excludedPlans = config('subscription.stripe_excluded_plans'); try {
$excludedPlans = config('subscription.stripe_excluded_plans');
$type = data_get($this->event, 'type'); $type = data_get($this->event, 'type');
$this->type = $type; $this->type = $type;
$data = data_get($this->event, 'data.object'); $data = data_get($this->event, 'data.object');
switch ($type) { switch ($type) {
case 'radar.early_fraud_warning.created': case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id'); $id = data_get($data, 'id');
$charge = data_get($data, 'charge'); $charge = data_get($data, 'charge');
if ($charge) { if ($charge) {
$stripe->refunds->create(['charge' => $charge]); $stripe->refunds->create(['charge' => $charge]);
} }
$pi = data_get($data, 'payment_intent'); $pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []); $piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer'); $customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \Exception("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \Exception("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \Exception("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) { if ($subscription) {
throw new \Exception("Subscription already exists for customer: {$customerId}"); $subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
} }
throw new \Exception('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \Exception("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break; break;
} case 'checkout.session.completed':
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $clientReferenceId = data_get($data, 'client_reference_id');
if (! $subscription) { if (is_null($clientReferenceId)) {
if ($status === 'incomplete_expired') { send_internal_notification('Checkout session completed without client reference id.');
send_internal_notification('Subscription incomplete expired'); break;
throw new \Exception('Subscription incomplete expired');
} }
if ($teamId) { $userId = Str::before($clientReferenceId, ':');
$subscription = Subscription::create([ $teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
}
throw new \RuntimeException('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId, 'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);
} else {
send_internal_notification('No subscription and team id found');
throw new \Exception('No subscription and team id found');
} }
} case 'customer.subscription.updated':
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $teamId = data_get($data, 'metadata.team_id');
$feedback = data_get($data, 'cancellation_details.feedback'); $userId = data_get($data, 'metadata.user_id');
$comment = data_get($data, 'cancellation_details.comment'); $customerId = data_get($data, 'customer');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key'); $status = data_get($data, 'status');
if (str($lookup_key)->contains('dynamic')) { $subscriptionId = data_get($data, 'items.data.0.subscription');
$quantity = data_get($data, 'items.data.0.quantity', 2); $planId = data_get($data, 'items.data.0.plan.id');
$team = data_get($subscription, 'team'); if (Str::contains($excludedPlans, $planId)) {
if ($team) { send_internal_notification('Subscription excluded.');
$team->update([ break;
'custom_server_limit' => $quantity, }
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired');
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
send_internal_notification('No subscription and team id found');
throw new \RuntimeException('No subscription and team id found');
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]); ]);
} }
ServerLimitCheckJob::dispatch($team); if ($feedback) {
} $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
$subscription->update([ if ($comment) {
'stripe_feedback' => $feedback, $reason .= ' with comment: \''.$comment."'";
'stripe_comment' => $comment, }
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
} }
} break;
break; case 'customer.subscription.deleted':
case 'customer.subscription.deleted': // End subscription
// End subscription $customerId = data_get($data, 'customer');
$customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team');
$team = data_get($subscription, 'team'); $team?->subscriptionEnded();
$team?->subscriptionEnded(); break;
break; default:
default: throw new \RuntimeException("Unhandled event type: {$type}");
// Unhandled event type }
} catch (\Exception $e) {
send_internal_notification('StripeProcessJob error: '.$e->getMessage());
} }
} }
} }

View File

@@ -35,7 +35,7 @@ class Docker extends Component
$this->network = new Cuid2; $this->network = new Cuid2;
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
if ($server_id) { if ($server_id) {
$this->selectedServer = $this->servers->find($server_id); $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
$this->serverId = $this->selectedServer->id; $this->serverId = $this->selectedServer->id;
} else { } else {
$this->selectedServer = $this->servers->first(); $this->selectedServer = $this->servers->first();

View File

@@ -71,7 +71,7 @@ class EnvironmentVariable extends Model
} }
} }
$environment_variable->update([ $environment_variable->update([
'version' => config('version'), 'version' => config('constants.coolify.version'),
]); ]);
}); });
static::saving(function (EnvironmentVariable $environmentVariable) { static::saving(function (EnvironmentVariable $environmentVariable) {

View File

@@ -42,8 +42,7 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'], 'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'],
'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'], 'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'],
'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'], 'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'],
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'], 'settings' => ['$ref' => '#/components/schemas/ServerSetting'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
] ]
)] )]
@@ -814,7 +813,7 @@ $schema://$host {
{ {
return Attribute::make( return Attribute::make(
get: function ($value) { get: function ($value) {
return preg_replace('/[^0-9]/', '', $value); return (int) preg_replace('/[^0-9]/', '', $value);
} }
); );
} }

View File

@@ -45,6 +45,8 @@ use OpenApi\Attributes as OA;
'wildcard_domain' => ['type' => 'string'], 'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'], 'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'], 'updated_at' => ['type' => 'string'],
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
] ]
)] )]
class ServerSetting extends Model class ServerSetting extends Model

View File

@@ -46,7 +46,7 @@ class DiscordMessage
public function toPayload(): array public function toPayload(): array
{ {
$footerText = 'Coolify v'.config('version'); $footerText = 'Coolify v'.config('constants.coolify.version');
if (isCloud()) { if (isCloud()) {
$footerText = 'Coolify Cloud'; $footerText = 'Coolify Cloud';
} }

View File

@@ -1,4 +1,4 @@
<?php <?php
$version = include 'config/version.php'; $version = include 'config/constants.php';
echo $version; echo $version['coolify']['version'] ?: 'unknown';

View File

@@ -192,7 +192,7 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('coolify.managed=true'); $labels->push('coolify.managed=true');
$labels->push('coolify.version='.config('version')); $labels->push('coolify.version='.config('constants.coolify.version'));
$labels->push('coolify.'.$type.'Id='.$id); $labels->push('coolify.'.$type.'Id='.$id);
$labels->push("coolify.type=$type"); $labels->push("coolify.type=$type");
$labels->push('coolify.name='.$name); $labels->push('coolify.name='.$name);

View File

@@ -1,3 +0,0 @@
<?php
return '4.0.0-beta.372';

View File

@@ -1,5 +1,5 @@
{ {
"openapi": "3.0.0", "openapi": "3.1.0",
"info": { "info": {
"title": "Coolify", "title": "Coolify",
"version": "0.1" "version": "0.1"
@@ -5391,6 +5391,17 @@
"summary": "Update", "summary": "Update",
"description": "Update Server.", "description": "Update Server.",
"operationId": "update-server-by-uuid", "operationId": "update-server-by-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Server UUID",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": { "requestBody": {
"description": "Server updated.", "description": "Server updated.",
"required": true, "required": true,
@@ -5451,10 +5462,7 @@
"content": { "content": {
"application\/json": { "application\/json": {
"schema": { "schema": {
"type": "array", "$ref": "#\/components\/schemas\/Server"
"items": {
"$ref": "#\/components\/schemas\/Server"
}
} }
} }
} }
@@ -7441,13 +7449,8 @@
"type": "string", "type": "string",
"description": "The swarm cluster configuration." "description": "The swarm cluster configuration."
}, },
"delete_unused_volumes": { "settings": {
"type": "boolean", "$ref": "#\/components\/schemas\/ServerSetting"
"description": "The flag to indicate if the unused volumes should be deleted."
},
"delete_unused_networks": {
"type": "boolean",
"description": "The flag to indicate if the unused networks should be deleted."
} }
}, },
"type": "object" "type": "object"
@@ -7556,6 +7559,14 @@
}, },
"updated_at": { "updated_at": {
"type": "string" "type": "string"
},
"delete_unused_volumes": {
"type": "boolean",
"description": "The flag to indicate if the unused volumes should be deleted."
},
"delete_unused_networks": {
"type": "boolean",
"description": "The flag to indicate if the unused networks should be deleted."
} }
}, },
"type": "object" "type": "object"

View File

@@ -3671,6 +3671,14 @@ paths:
summary: Update summary: Update
description: 'Update Server.' description: 'Update Server.'
operationId: update-server-by-uuid operationId: update-server-by-uuid
parameters:
-
name: uuid
in: path
description: 'Server UUID'
required: true
schema:
type: string
requestBody: requestBody:
description: 'Server updated.' description: 'Server updated.'
required: true required: true
@@ -3713,9 +3721,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array $ref: '#/components/schemas/Server'
items:
$ref: '#/components/schemas/Server'
'401': '401':
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
'400': '400':
@@ -4967,12 +4973,8 @@ components:
swarm_cluster: swarm_cluster:
type: string type: string
description: 'The swarm cluster configuration.' description: 'The swarm cluster configuration.'
delete_unused_volumes: settings:
type: boolean $ref: '#/components/schemas/ServerSetting'
description: 'The flag to indicate if the unused volumes should be deleted.'
delete_unused_networks:
type: boolean
description: 'The flag to indicate if the unused networks should be deleted.'
type: object type: object
ServerSetting: ServerSetting:
description: 'Server Settings model' description: 'Server Settings model'
@@ -5045,6 +5047,12 @@ components:
type: string type: string
updated_at: updated_at:
type: string type: string
delete_unused_volumes:
type: boolean
description: 'The flag to indicate if the unused volumes should be deleted.'
delete_unused_networks:
type: boolean
description: 'The flag to indicate if the unused networks should be deleted.'
type: object type: object
Service: Service:
description: 'Service model' description: 'Service model'

View File

@@ -60,7 +60,22 @@ export function initializeTerminalComponent() {
}; };
}, },
resetTerminal() {
if (this.term) {
this.$wire.dispatch('error', 'Terminal websocket connection lost.');
this.term.reset();
this.term.clear();
this.pendingWrites = 0;
this.paused = false;
this.commandBuffer = '';
// Force a refresh
this.$nextTick(() => {
this.resizeTerminal();
this.term.focus();
});
}
},
setupTerminal() { setupTerminal() {
const terminalElement = document.getElementById('terminal'); const terminalElement = document.getElementById('terminal');
if (terminalElement) { if (terminalElement) {
@@ -69,9 +84,15 @@ export function initializeTerminalComponent() {
rows: 30, rows: 30,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
cursorBlink: true, cursorBlink: true,
rendererType: 'canvas',
convertEol: true,
disableStdin: false
}); });
this.fitAddon = new FitAddon(); this.fitAddon = new FitAddon();
this.term.loadAddon(this.fitAddon); this.term.loadAddon(this.fitAddon);
this.$nextTick(() => {
this.resizeTerminal();
});
} }
}, },
@@ -101,12 +122,19 @@ export function initializeTerminalComponent() {
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}` `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
this.socket = new WebSocket(url); this.socket = new WebSocket(url);
this.socket.onopen = () => {
console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');
};
this.socket.onmessage = this.handleSocketMessage.bind(this); this.socket.onmessage = this.handleSocketMessage.bind(this);
this.socket.onerror = (e) => { this.socket.onerror = (e) => {
console.error('WebSocket error:', e); console.error('[Terminal] WebSocket error.');
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
console.log('WebSocket connection closed'); console.warn('[Terminal] WebSocket connection closed.');
this.resetTerminal();
this.message = '(connection closed)';
this.terminalActive = false;
this.reconnect(); this.reconnect();
}; };
} }
@@ -117,19 +145,18 @@ export function initializeTerminalComponent() {
clearInterval(this.reconnectInterval); clearInterval(this.reconnectInterval);
} }
this.reconnectInterval = setInterval(() => { this.reconnectInterval = setInterval(() => {
console.log('Attempting to reconnect...'); console.warn('[Terminal] Attempting to reconnect...');
this.initializeWebSocket(); this.initializeWebSocket();
if (this.socket && this.socket.readyState === WebSocket.OPEN) { if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.log('Reconnected successfully'); console.log('[Terminal] Reconnected successfully');
clearInterval(this.reconnectInterval); clearInterval(this.reconnectInterval);
this.reconnectInterval = null; this.reconnectInterval = null;
window.location.reload();
} }
}, 2000); }, 2000);
}, },
handleSocketMessage(event) { handleSocketMessage(event) {
this.message = '(connection closed)';
if (event.data === 'pty-ready') { if (event.data === 'pty-ready') {
if (!this.term._initialized) { if (!this.term._initialized) {
this.term.open(document.getElementById('terminal')); this.term.open(document.getElementById('terminal'));
@@ -150,8 +177,17 @@ export function initializeTerminalComponent() {
this.term.reset(); this.term.reset();
this.commandBuffer = ''; this.commandBuffer = '';
} else { } else {
this.pendingWrites++; try {
this.term.write(event.data, this.flowControlCallback.bind(this)); this.pendingWrites++;
this.term.write(event.data, (err) => {
if (err) {
console.error('[Terminal] Write error:', err);
}
this.flowControlCallback();
});
} catch (error) {
console.error('[Terminal] Write operation failed:', error);
}
} }
}, },
@@ -173,11 +209,15 @@ export function initializeTerminalComponent() {
if (!this.term) return; if (!this.term) return;
this.term.onData((data) => { this.term.onData((data) => {
this.socket.send(JSON.stringify({ message: data })); if (this.socket.readyState === WebSocket.OPEN) {
if (data === '\r') { this.socket.send(JSON.stringify({ message: data }));
this.commandBuffer = ''; if (data === '\r') {
this.commandBuffer = '';
} else {
this.commandBuffer += data;
}
} else { } else {
this.commandBuffer += data; console.warn('[Terminal] WebSocket not ready, data not sent');
} }
}); });

View File

@@ -11,16 +11,20 @@
toast(this.title, { description: this.description, type: this.type, position: this.position, html: html }) toast(this.title, { description: this.description, type: this.type, position: this.position, html: html })
} }
}" x-init="window.toast = function(message, options = {}) { }" x-init="window.toast = function(message, options = {}) {
let description = ''; try {
let type = 'default'; let description = '';
let position = 'top-center'; let type = 'default';
let html = ''; let position = 'top-center';
if (typeof options.description != 'undefined') description = options.description; let html = '';
if (typeof options.type != 'undefined') type = options.type; if (typeof options.description != 'undefined') description = options.description;
if (typeof options.position != 'undefined') position = options.position; if (typeof options.type != 'undefined') type = options.type;
if (typeof options.html != 'undefined') html = options.html; if (typeof options.position != 'undefined') position = options.position;
if (typeof options.html != 'undefined') html = options.html;
window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } })); window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } }));
} catch (error) {
console.error('Error showing toast:', error);
}
}" class="relative space-y-5"> }" class="relative space-y-5">
<template x-teleport="body"> <template x-teleport="body">
<ul x-data="{ <ul x-data="{

View File

@@ -1,4 +1,4 @@
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-90 hover:opacity-100 dark:hover:text-white hover:text-black']) }} <a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-90 hover:opacity-100 dark:hover:text-white hover:text-black']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('version') }}" target="_blank"> href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}" target="_blank">
v{{ config('version') }} v{{ config('constants.coolify.version') }}
</a> </a>

View File

@@ -96,6 +96,20 @@
enableStats: false, enableStats: false,
enableLogging: true, enableLogging: true,
enabledTransports: ['ws', 'wss'], enabledTransports: ['ws', 'wss'],
disableStats: true,
// Add auto reconnection settings
enabledTransports: ['ws', 'wss'],
disabledTransports: ['sockjs', 'xhr_streaming', 'xhr_polling'],
// Attempt to reconnect on connection lost
autoReconnect: true,
// Wait 1 second before first reconnect attempt
reconnectionDelay: 1000,
// Maximum delay between reconnection attempts
maxReconnectionDelay: 1000,
// Multiply delay by this number for each reconnection attempt
reconnectionDelayGrowth: 1,
// Maximum number of reconnection attempts
maxAttempts: 15
}); });
@endauth @endauth
let checkHealthInterval = null; let checkHealthInterval = null;

View File

@@ -11,16 +11,22 @@
let checkNumber = 1; let checkNumber = 1;
let checkPusherInterval = null; let checkPusherInterval = null;
let checkReconnectInterval = null;
if (!this.popups.realtime) { if (!this.popups.realtime) {
checkPusherInterval = setInterval(() => { checkPusherInterval = setInterval(() => {
if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') { if (window.Echo) {
checkNumber++; if (window.Echo.connector.pusher.connection.state === 'connected') {
if (checkNumber > 5) { this.popups.realtime = false;
this.popups.realtime = true; } else {
console.error( checkNumber++;
'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)' if (checkNumber > 5) {
); this.popups.realtime = true;
clearInterval(checkPusherInterval); console.error(
'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)'
);
}
} }
} }
}, 2000); }, 2000);

View File

@@ -26,8 +26,8 @@
@else @else
@if (count($containers) > 0) @if (count($containers) > 0)
@if (count($containers) === 1) @if (count($containers) === 1)
<form class="w-full pt-4" <form class="w-full pt-4" wire:submit="$dispatchSelf('connectToContainer')"
wire:submit="$dispatchSelf('connectToContainer')" wire:init="$dispatchSelf('connectToContainer')"> wire:init="$dispatchSelf('connectToContainer')">
<x-forms.button class="w-full" type="submit">Reconnect</x-forms.button> <x-forms.button class="w-full" type="submit">Reconnect</x-forms.button>
</form> </form>
@else @else