Compare commits

...

69 Commits

Author SHA1 Message Date
Andras Bacsai
2468251f56 Merge pull request #1783 from coollabsio/next
v4.0.0-beta.226
2024-02-26 14:31:16 +01:00
Andras Bacsai
6e74f3e40e Merge pull request #1779 from Rei-x/next
Fix import to mysql and mariadb
2024-02-26 14:30:35 +01:00
Andras Bacsai
407f84a4bb Refactor Dockerfile location handling in ApplicationDeploymentJob.php 2024-02-26 14:28:02 +01:00
Andras Bacsai
91632f0adb fix: custom dockerfile location always checked 2024-02-26 14:26:19 +01:00
Andras Bacsai
af3c575d84 fix: server disabled 2024-02-26 14:22:24 +01:00
Andras Bacsai
bf1475441d Update service stop message and fix sidebar alignment 2024-02-26 12:38:15 +01:00
Andras Bacsai
9268f9db1d Refactor user switching logic and update UI 2024-02-26 11:48:35 +01:00
Andras Bacsai
600c43827a Update server check and version numbers 2024-02-26 11:25:38 +01:00
Andras Bacsai
74092ea95b Merge pull request #1776 from coollabsio/next
4.0.0-beta.225
2024-02-26 11:08:53 +01:00
Andras Bacsai
b67abe58e8 Remove commented out code in ServerStatusJob.php 2024-02-26 10:34:44 +01:00
Andras Bacsai
678647f39a fix: force enable/disable server in case ultimate package quantity decreases 2024-02-26 10:25:21 +01:00
Andras Bacsai
453956172b Refactor show.blade.php to improve code readability 2024-02-26 09:32:28 +01:00
Andras Bacsai
b550c32f9b Add whitespace-pre-line class to font-mono in deployment show blade file 2024-02-26 09:09:01 +01:00
Andras Bacsai
f6b886adbc revert delayed jobs for now 2024-02-26 08:52:46 +01:00
Andras Bacsai
9642453052 fix: firefly service 2024-02-26 08:52:17 +01:00
Andras Bacsai
64fca99c26 feat: server disabled by overflow 2024-02-25 23:34:01 +01:00
Andras Bacsai
c7da43f50d feat: add static ipv4 ipv6 support 2024-02-25 23:13:27 +01:00
Rei
6efa2dd9ba fix: import to mysql and mariadb 2024-02-25 22:15:48 +01:00
Andras Bacsai
5e980c5fe0 Update pricing plans layout and text 2024-02-25 22:14:20 +01:00
Andras Bacsai
c8c7a415ea Add new Livewire component and update subscription actions 2024-02-25 22:08:44 +01:00
Andras Bacsai
c3cfb8d23b Refactor getRecepients method and fix serverLimitReached method in Team model 2024-02-25 18:22:24 +01:00
Andras Bacsai
1b055f0316 Refactor subscription pricing and update server limit 2024-02-25 14:00:35 +01:00
Andras Bacsai
1fcbf0b363 Update pricing plans display and button text 2024-02-23 22:14:24 +01:00
Andras Bacsai
61dbc81765 feat: delay container/server jobs 2024-02-23 21:51:43 +01:00
Andras Bacsai
b8b76dfa40 Refactor CleanupQueue to CleanupDatabase 2024-02-23 21:05:48 +01:00
Andras Bacsai
297b314904 feat: custom server limit 2024-02-23 15:45:53 +01:00
Andras Bacsai
55dd1ab0a1 Update cleanup script and version numbers 2024-02-23 14:39:52 +01:00
Andras Bacsai
8c803f1c4b Merge pull request #1775 from coollabsio/next
v4.0.0-beta.224
2024-02-23 13:57:11 +01:00
Andras Bacsai
3b942049a2 Refactor subscription handling logic in middleware and model 2024-02-23 13:50:48 +01:00
Andras Bacsai
f78fd212bb fix: subscription / plan switch, etc 2024-02-23 12:59:14 +01:00
Andras Bacsai
f931ebece8 feat: make user owner
fix: ownership check
2024-02-23 12:34:36 +01:00
Andras Bacsai
ea0a9763bf Update navbar icons 2024-02-23 11:23:14 +01:00
Andras Bacsai
b59e47dcf9 fix: stripe invoice paid webhook
fix: prepare customer initiated tier change
fix: separate view for subscriptions
2024-02-23 11:21:14 +01:00
Andras Bacsai
ce09ef8848 Merge pull request #1774 from steveworley/fix/ux-hamburger-extra-menu-options
Fix: Change + icon to hamburger.
2024-02-23 10:18:38 +01:00
Andras Bacsai
188727daba Update version numbers 2024-02-23 10:14:32 +01:00
Andras Bacsai
1150633fef fix: unknown image of service until it is uploaded 2024-02-23 10:14:13 +01:00
Andras Bacsai
62ae845f4b fix: complex service status
service: firefly III
2024-02-23 10:09:42 +01:00
Steve Worley
0757fd741e Fix: Change + icon to hamburger. 2024-02-23 09:21:11 +10:00
Andras Bacsai
a07fa8ccd2 Merge pull request #1773 from coollabsio/next
v4.0.0-beta.223
2024-02-22 15:23:27 +01:00
Andras Bacsai
c77f32e696 fix: statuses 2024-02-22 15:15:16 +01:00
Andras Bacsai
4391771416 Merge pull request #1768 from coollabsio/next
v4.0.0-beta.222
2024-02-22 14:56:55 +01:00
Andras Bacsai
01ab820459 Fix error message formatting in handleError function 2024-02-22 14:56:41 +01:00
Andras Bacsai
c7218f2856 Update success messages 2024-02-22 14:53:42 +01:00
Andras Bacsai
592221b4bf fix: server validation 2024-02-22 14:46:11 +01:00
Andras Bacsai
836458ad85 fix: no coolify.yaml found 2024-02-22 14:45:56 +01:00
Andras Bacsai
7233c86f3d fix: use latest image if nothing is specified 2024-02-22 14:45:41 +01:00
Andras Bacsai
154b1b05e4 feat: able to add dynamic configurations from proxy dashboard 2024-02-22 13:29:28 +01:00
Andras Bacsai
4d88638d4d Update proxy configuration in bootstrap/helpers/proxy.php 2024-02-22 12:00:16 +01:00
Andras Bacsai
9403986643 Merge pull request #1767 from iamEvanYT/fix-stuck-connections
fix: connections being stuck and not processed until proxy restarts
2024-02-22 11:59:52 +01:00
Andras Bacsai
ad48610b2f Merge pull request #1770 from piscis/patch-2
fix: Avoid breaking the tile layout with long fqdn output
2024-02-22 11:54:25 +01:00
Andras Bacsai
63487cf3ec feat: minversion for services 2024-02-22 11:53:25 +01:00
Andras Bacsai
4ae2087c2e fix: server validation 2024-02-22 11:28:45 +01:00
Andras Bacsai
5179129a6b fix: complex container status
feat: able to change primary server
feat: links inside the logs
2024-02-22 10:57:05 +01:00
Andras Bacsai
6a00d8c88c Refactor loadData method in Destination.php 2024-02-22 09:38:09 +01:00
Andras Bacsai
50f43f9396 Update resource view to set type as 'public' 2024-02-22 09:38:03 +01:00
Alex
6f205f8931 Force browser to break all words on line end for fqdn output
Force the browser to break long lines for the fqdn output instead of overflowing the tile
2024-02-21 19:16:09 +01:00
Andras Bacsai
3776ffa49b disable administration gh permission for now 2024-02-21 14:42:38 +01:00
Andras Bacsai
318b5beac1 Update select.blade.php with responsive styling 2024-02-21 14:40:48 +01:00
Andras Bacsai
344c5d6d12 Update service configurations 2024-02-21 14:30:32 +01:00
Andras Bacsai
4d319a8caa Update service and shared helper files 2024-02-21 12:22:32 +01:00
Andras Bacsai
74b24a0690 Add file permission change for LocalFileVolume.php and add service_name parameter to fqdnLabelsForTraefik() function 2024-02-21 11:21:11 +01:00
Andras Bacsai
1ca0464957 fix: permission change updates from webhook 2024-02-20 20:17:04 +01:00
Andras Bacsai
f7ebc8a88c feat: save github app permission locally 2024-02-20 18:14:47 +01:00
Andras Bacsai
a102099ac1 icons 2024-02-20 17:08:16 +01:00
Andras Bacsai
59ac22aa05 Update redis.svg icon 2024-02-20 15:45:30 +01:00
Andras Bacsai
cd7244b3d7 Add logos for various services 2024-02-20 15:42:30 +01:00
Andras Bacsai
f81b676abe ui: updates 2024-02-20 15:07:12 +01:00
iamEvan
234b154053 fix: connections being stuck and not processed until proxy restarts 2024-02-20 17:16:43 +08:00
Andras Bacsai
a1d09ad574 Update version numbers to 4.0.0-beta.222 2024-02-19 13:39:05 +01:00
247 changed files with 5381 additions and 1060 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Console\Commands;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command
{
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue';
public function handle()
{
$team_id = $this->option('team-id');
$servers = \App\Models\Server::where('team_id', $team_id)->get();
foreach ($servers as $server) {
$deployments = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->where("server_id", $server->id)->get();
foreach ($deployments as $deployment) {
$deployment->update(['status' => 'failed']);
instant_remote_process(['docker rm -f ' . $deployment->deployment_uuid], $server, false);
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class CleanupDatabase extends Command
{
protected $signature = 'cleanup:database {--yes}';
protected $description = 'Cleanup database';
public function handle()
{
echo "Running database cleanup...\n";
$keep_days = 60;
// Cleanup failed jobs table
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7));
$count = $failed_jobs->count();
echo "Delete $count entries from failed_jobs.\n";
if ($this->option('yes')) {
$failed_jobs->delete();
}
// Cleanup sessions table
$sessions = DB::table('sessions')->where('last_activity', '<', now()->subDays($keep_days)->timestamp);
$count = $sessions->count();
echo "Delete $count entries from sessions.\n";
if ($this->option('yes')) {
$sessions->delete();
}
// Cleanup activity_log table
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days));
$count = $activity_log->count();
echo "Delete $count entries from activity_log.\n";
if ($this->option('yes')) {
$activity_log->delete();
}
// Cleanup application_deployment_queues table
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days));
$count = $application_deployment_queues->count();
echo "Delete $count entries from application_deployment_queues.\n";
if ($this->option('yes')) {
$application_deployment_queues->delete();
}
// Cleanup webhooks table
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
$count = $webhooks->count();
echo "Delete $count entries from webhooks.\n";
if ($this->option('yes')) {
$webhooks->delete();
}
}
}

View File

@@ -8,15 +8,16 @@ use Illuminate\Console\Command;
class CleanupUnreachableServers extends Command
{
protected $signature = 'cleanup:unreachable-servers';
protected $description = 'Cleanup Unreachable Servers (3 days)';
protected $description = 'Cleanup Unreachable Servers (7 days)';
public function handle()
{
echo "Running unreachable server cleanup...\n";
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([
'ip' => '1.2.3.4'
]);

View File

@@ -73,6 +73,18 @@ class ServicesGenerate extends Command
} else {
$slogan = str($file)->headline()->value();
}
$logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values();
if ($logo->count() > 0) {
$logo = str($logo[0])->after('# logo:')->trim()->value();
} else {
$logo = 'svgs/unknown.svg';
}
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
if ($minversion->count() > 0) {
$minversion = str($minversion[0])->after('# minversion:')->trim()->value();
} else {
$minversion = '0.0.0';
}
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
if ($env_file->count() > 0) {
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
@@ -96,6 +108,8 @@ class ServicesGenerate extends Command
'slogan' => $slogan,
'compose' => $yaml,
'tags' => $tags,
'logo' => $logo,
'minversion' => $minversion,
];
if ($env_file) {
$env_file_content = file_get_contents(base_path("templates/compose/$env_file"));

View File

@@ -4,7 +4,6 @@ namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\ComplexContainerStatusJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\InstanceAutoUpdateJob;
@@ -70,12 +69,34 @@ class Kernel extends ConsoleKernel
}
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ContainerStatusJob($server);
// $job->delay($randomSeconds);
// ray('dispatching container status job in ' . $randomSeconds . ' seconds');
// dispatch($job);
// })->name('container-status-' . $server->id)->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new CheckLogDrainContainerJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ServerStatusJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)

View File

@@ -14,7 +14,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
class APIDeploy extends Controller
{
public function deploy(Request $request)
{

View File

@@ -6,7 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Project extends Controller
class APIProject extends Controller
{
public function projects(Request $request)
{

View File

@@ -6,7 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
class Server extends Controller
class APIServer extends Controller
{
public function servers(Request $request)
{

View File

@@ -44,7 +44,7 @@ class DecideWhatToDoWithUser
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect(RouteServiceProvider::HOME);
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
if (isSubscriptionActive() && $request->routeIs('subscription.index')) {
return redirect(RouteServiceProvider::HOME);
}
return $next($request);

View File

@@ -66,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private Server $mainServer;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
private bool $only_this_server = false;
private string $container_name;
private ?string $currently_running_container_name = null;
@@ -115,6 +116,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->commit = $this->application_deployment_queue->commit;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->only_this_server = $this->application_deployment_queue->only_this_server;
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
@@ -165,65 +167,71 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
}
if ($this->application->build_pack === 'dockerfile') {
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
}
}
}
public function handle(): void
{
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
if (!is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
$allContainers = collect($allContainers)->sort()->values();
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
if (preg_match('/-(\d{12})/', $containerName)) {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
try {
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
if (!is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
$allContainers = collect($allContainers)->sort()->values();
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
if (preg_match('/-(\d{12})/', $containerName)) {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Check custom port
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
// Check custom port
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
if (data_get($this->application, 'settings.is_build_server_enabled')) {
$teamId = data_get($this->application, 'environment.project.team.id');
$buildServers = Server::buildServers($teamId)->get();
if ($buildServers->count() === 0) {
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
if (data_get($this->application, 'settings.is_build_server_enabled')) {
$teamId = data_get($this->application, 'environment.project.team.id');
$buildServers = Server::buildServers($teamId)->get();
if ($buildServers->count() === 0) {
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
$this->build_server = $this->server;
$this->original_server = $this->server;
} else {
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
$this->build_server = $buildServers->random();
$this->original_server = $this->server;
$this->use_build_server = true;
}
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server;
$this->original_server = $this->server;
} else {
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
$this->build_server = $buildServers->random();
$this->original_server = $this->server;
$this->use_build_server = true;
}
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server;
$this->original_server = $this->server;
}
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
if ($this->server->isProxyShouldRun()) {
@@ -331,7 +339,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function deploy_dockerimage_buildpack()
{
$this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag;
if (str($this->application->docker_registry_image_tag)->isEmpty()) {
$this->dockerImageTag = 'latest';
} else {
$this->dockerImageTag = $this->application->docker_registry_image_tag;
}
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
$this->generate_image_names();
@@ -887,7 +899,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
destination: $destination,
no_questions_asked: true,
);
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
$this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: " . route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
@@ -1217,6 +1229,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
}
} else {
@@ -1224,6 +1249,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
}
@@ -1619,13 +1657,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->deploy_to_additional_destinations();
if (!$this->only_this_server) {
$this->deploy_to_additional_destinations();
}
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}
public function failed(Throwable $exception): void
{
$this->next(ApplicationDeploymentStatus::FAILED->value);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
if (str($exception->getMessage())->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
@@ -1633,6 +1675,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack !== 'dockercompose') {
$code = $exception->getCode();
ray($code);
if ($code !== 69420) {
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
@@ -1641,7 +1684,5 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
);
}
}
$this->next(ApplicationDeploymentStatus::FAILED->value);
}
}

View File

@@ -43,22 +43,24 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
$applications = $this->server->applications();
foreach ($applications as $application) {
if ($application->additional_servers->count() > 0) {
$is_main_server = $application->destination->server->id === $this->server->id;
if ($is_main_server) {
ComplexStatusCheck::run($application);
$applications = $applications->filter(function ($value, $key) use ($application) {
return $value->id !== $application->id;
});
}
}
}
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
};
$applications = $this->server->applications();
$skip_these_applications = collect([]);
foreach ($applications as $application) {
if ($application->additional_servers->count() > 0) {
$skip_these_applications->push($application);
ComplexStatusCheck::run($application);
$applications = $applications->filter(function ($value, $key) use ($application) {
return $value->id !== $application->id;
});
}
}
$applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
return !$skip_these_applications->pluck('id')->contains($value->id);
});
try {
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Models\GithubApp;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class GithubAppPermissionJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public GithubApp $github_app)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->github_app->uuid))];
}
public function uniqueId(): int
{
return $this->github_app->uuid;
}
public function handle()
{
try {
$github_access_token = generate_github_jwt_token($this->github_app);
$response = Http::withHeaders([
'Authorization' => "Bearer $github_access_token",
'Accept' => 'application/vnd.github+json'
])->get("{$this->github_app->api_url}/app");
$response = $response->json();
$permissions = data_get($response, 'permissions');
$this->github_app->contents = data_get($permissions, 'contents');
$this->github_app->metadata = data_get($permissions, 'metadata');
$this->github_app->pull_requests = data_get($permissions, 'pull_requests');
$this->github_app->administration = data_get($permissions, 'administration');
$this->github_app->save();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
} catch (\Throwable $e) {
send_internal_notification('GithubAppPermissionJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use App\Notifications\Server\ForceDisabled;
use App\Notifications\Server\ForceEnabled;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Team $team)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle()
{
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
$limit = $this->team->limits['serverLimit'];
$number_of_servers_to_disable = $servers_count - $limit;
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
if ($number_of_servers_to_disable > 0) {
ray('Disabling servers');
$servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable);
$servers_to_disable->each(function ($server) {
$server->forceDisableServer();
$this->team->notify(new ForceDisabled($server));
});
} else if ($number_of_servers_to_disable === 0) {
$servers->each(function ($server) {
if ($server->isForceDisabled()) {
$server->forceEnableServer();
$this->team->notify(new ForceEnabled($server));
}
});
}
} catch (\Throwable $e) {
send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage());
ray($e->getMessage());
return handleError($e);
}
}
}

View File

@@ -41,15 +41,6 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
throw new \RuntimeException('Server is not ready.');
};
try {
// $this->server->validateConnection();
// $this->server->validateOS();
// $docker_installed = $this->server->validateDockerEngine();
// if (!$docker_installed) {
// $this->server->installDocker();
// $this->server->validateDockerEngine();
// }
// $this->server->validateDockerEngineVersion();
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Admin;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component;
@@ -14,28 +15,26 @@ class Index extends Component
if (!isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0 && session('adminToken') === null) {
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
$this->users = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get();
})->get()->filter(function ($user) {
return $user->id !== 0;
});
}
public function switchUser(int $user_id)
{
$user = User::find($user_id);
auth()->login($user);
if ($user_id === 0) {
session()->forget('adminToken');
} else {
$token_payload = [
'valid' => true,
];
$token = Crypt::encrypt($token_payload);
session(['adminToken' => $token]);
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
return refreshSession();
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
auth()->login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
public function render()
{

View File

@@ -23,8 +23,8 @@ class Dashboard extends Component
public function cleanup_queue()
{
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('app:init', [
'--cleanup-deployments' => 'true'
Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id
]);
}
public function get_deployments()

View File

@@ -4,7 +4,7 @@ namespace App\Livewire;
use Livewire\Component;
class Sponsorship extends Component
class LayoutPopups extends Component
{
public function getListeners()
{
@@ -23,6 +23,6 @@ class Sponsorship extends Component
}
public function render()
{
return view('livewire.sponsorship');
return view('livewire.layout-popups');
}
}

View File

@@ -63,7 +63,7 @@ class EmailSettings extends Component
]);
$this->team->save();
refreshSession();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -71,7 +71,7 @@ class EmailSettings extends Component
public function sendTestNotification()
{
$this->team?->notify(new Test($this->emails));
$this->dispatch('success', 'Test Email sent successfully.');
$this->dispatch('success', 'Test Email sent.');
}
public function instantSaveInstance()
{
@@ -83,7 +83,7 @@ class EmailSettings extends Component
$this->team->resend_enabled = false;
$this->team->save();
refreshSession();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -131,7 +131,7 @@ class EmailSettings extends Component
]);
$this->team->save();
refreshSession();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return handleError($e, $this);
@@ -148,7 +148,7 @@ class EmailSettings extends Component
]);
$this->team->save();
refreshSession();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
$this->team->resend_enabled = false;
return handleError($e, $this);

View File

@@ -27,7 +27,7 @@ class Index extends Component
'name' => $this->name,
]);
$this->dispatch('success', 'Profile updated successfully.');
$this->dispatch('success', 'Profile updated');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -232,7 +232,6 @@ class General extends Component
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required',
]);
}
if (data_get($this->application, 'fqdn')) {

View File

@@ -33,15 +33,11 @@ class Heading extends Component
{
if ($this->application->destination->server->isFunctional()) {
dispatch(new ContainerStatusJob($this->application->destination->server));
// $this->application->refresh();
// $this->application->previews->each(function ($preview) {
// $preview->refresh();
// });
} else {
dispatch(new ServerStatusJob($this->application->destination->server));
}
if ($showNotification) $this->dispatch('success', "Application status updated.");
if ($showNotification) $this->dispatch('success', "Success", "Application status updated.");
}
public function force_deploy_without_cache()

View File

@@ -46,7 +46,7 @@ class Form extends Component
$this->validate();
$this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template);
$this->application->save();
$this->dispatch('success', 'Preview url template updated successfully.');
$this->dispatch('success', 'Preview url template updated.');
$this->generate_real_url();
}
}

View File

@@ -33,7 +33,7 @@ class BackupExecutions extends Component
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
$execution->delete();
$this->dispatch('success', 'Backup deleted successfully.');
$this->dispatch('success', 'Backup deleted.');
$this->dispatch('refreshBackupExecutions');
}
public function download($exeuctionId)

View File

@@ -29,8 +29,8 @@ class Import extends Component
public string $container;
public array $importCommands = [];
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
public function getListeners()
{

View File

@@ -59,7 +59,7 @@ class General extends Component
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -73,7 +73,7 @@ class General extends Component
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
}

View File

@@ -58,7 +58,7 @@ class General extends Component
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -75,7 +75,7 @@ class General extends Component
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
}

View File

@@ -60,7 +60,7 @@ class General extends Component
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -74,7 +74,7 @@ class General extends Component
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
}

View File

@@ -66,7 +66,7 @@ class General extends Component
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -105,7 +105,7 @@ class General extends Component
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);
$this->database->init_scripts = array_merge($this->database->init_scripts, [$script]);
$this->database->save();
$this->dispatch('success', 'Init script saved successfully.');
$this->dispatch('success', 'Init script saved.');
}
public function delete_init_script($script)
@@ -116,7 +116,7 @@ class General extends Component
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray();
$this->database->save();
$this->refresh();
$this->dispatch('success', 'Init script deleted successfully.');
$this->dispatch('success', 'Init script deleted.');
return;
}
}
@@ -148,7 +148,7 @@ class General extends Component
]
]);
$this->database->save();
$this->dispatch('success', 'Init script added successfully.');
$this->dispatch('success', 'Init script added.');
$this->new_content = '';
$this->new_filename = '';
}
@@ -161,7 +161,7 @@ class General extends Component
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
}

View File

@@ -52,7 +52,7 @@ class General extends Component
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -66,7 +66,7 @@ class General extends Component
$this->database->redis_conf = null;
}
$this->database->save();
$this->dispatch('success', 'Database updated successfully.');
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
}

View File

@@ -38,7 +38,7 @@ class ScheduledBackups extends Component
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
$this->dispatch('success', 'Scheduled backup deleted successfully.');
$this->dispatch('success', 'Scheduled backup deleted.');
$this->refreshScheduledBackups();
}

View File

@@ -71,10 +71,10 @@ class Select extends Component
// }
// }
public function loadServices()
public function loadServices(bool $force = false)
{
try {
if (count($this->allServices) > 0) {
if (count($this->allServices) > 0 && !$force) {
if (!$this->search) {
$this->services = $this->allServices;
return;

View File

@@ -77,7 +77,7 @@ class Database extends Component
$this->validate();
$this->database->save();
updateCompose($this->database);
$this->dispatch('success', 'Database saved successfully.');
$this->dispatch('success', 'Database saved.');
} catch (\Throwable $e) {
ray($e);
} finally {

View File

@@ -42,7 +42,7 @@ class FileStorage extends Component
}
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
$this->dispatch('success', 'File updated successfully.');
$this->dispatch('success', 'File updated.');
} catch (\Throwable $e) {
$this->fileStorage->setRawAttributes($original);
$this->fileStorage->save();

View File

@@ -64,9 +64,9 @@ class Navbar extends Component
StopService::run($this->service);
$this->service->refresh();
if ($forceCleanup) {
$this->dispatch('success', 'Force cleanup service successfully.');
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped successfully.');
$this->dispatch('success', 'Service stopped.');
}
ServiceStatusChanged::dispatch();
}

View File

@@ -41,7 +41,7 @@ class ServiceApplicationView extends Component
{
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted successfully.');
$this->dispatch('success', 'Application deleted.');
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -58,7 +58,7 @@ class ServiceApplicationView extends Component
$this->validate();
$this->application->save();
updateCompose($this->application);
$this->dispatch('success', 'Application saved successfully.');
$this->dispatch('success', 'Application saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {

View File

@@ -48,7 +48,7 @@ class StackForm extends Component
public function instantSave()
{
$this->service->save();
$this->dispatch('success', 'Service settings saved successfully.');
$this->dispatch('success', 'Service settings saved.');
}
public function submit()
@@ -62,7 +62,7 @@ class StackForm extends Component
$this->service->saveComposeConfigs();
$this->dispatch('refreshStacks');
$this->dispatch('refreshEnvs');
$this->dispatch('success', 'Service saved successfully.');
$this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
@@ -40,6 +41,17 @@ class Destination extends Component
$this->networks = $this->networks->reject(function ($network) {
return $this->resource->destination->server->id == $network->server->id;
});
if ($this->resource?->additional_servers?->count() > 0) {
$this->networks = $this->networks->reject(function ($network) {
return $this->resource->additional_servers->pluck('id')->contains($network->server->id);
});
}
}
public function stop(int $server_id)
{
$server = Server::find($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
}
public function redeploy(int $network_id, int $server_id)
{
@@ -55,6 +67,7 @@ class Destination extends Component
application: $this->resource,
server: $server,
destination: $destination,
only_this_server: true,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
@@ -64,12 +77,29 @@ class Destination extends Component
'environment_name' => data_get($this->resource, 'environment.name'),
]);
}
public function promote(int $network_id, int $server_id)
{
$main_destination = $this->resource->destination;
$this->resource->update([
'destination_id' => $network_id,
'destination_type' => StandaloneDocker::class,
]);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
$this->refreshServers();
}
public function refreshServers()
{
ContainerStatusJob::dispatchSync($this->resource->destination->server);
$this->loadData();
$this->dispatch('refresh');
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
public function addServer(int $network_id, int $server_id)
{
$this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
public function removeServer(int $network_id, int $server_id)
{
@@ -80,8 +110,7 @@ class Destination extends Component
$server = Server::find($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
}

View File

@@ -120,9 +120,9 @@ class All extends Component
}
}
if ($isPreview) {
$this->dispatch('success', 'Preview environment variables updated successfully.');
$this->dispatch('success', 'Preview environment variables updated.');
} else {
$this->dispatch('success', 'Environment variables updated successfully.');
$this->dispatch('success', 'Environment variables updated.');
}
$this->refreshEnvs();
}

View File

@@ -90,7 +90,7 @@ class Show extends Component
}
$this->serialize();
$this->env->save();
$this->dispatch('success', 'Environment variable updated successfully.');
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('refreshEnvs');
} catch (\Exception $e) {
return handleError($e);

View File

@@ -107,6 +107,9 @@ class ExecuteContainerCommand extends Component
{
$this->validate();
try {
if ($this->server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
// Wrap command to prevent escaped execution in the host.
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"';
if (!empty($this->workDir)) {

View File

@@ -52,7 +52,7 @@ class ResourceLimits extends Component
}
$this->validate();
$this->resource->save();
$this->dispatch('success', 'Resource limits updated successfully.');
$this->dispatch('success', 'Resource limits updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -48,7 +48,7 @@ class All extends Component
}
$task->save();
$this->refreshTasks();
$this->dispatch('success', 'Scheduled task added successfully.');
$this->dispatch('success', 'Scheduled task added.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -49,7 +49,7 @@ class Show extends Component
{
$this->validate();
$this->task->save();
$this->dispatch('success', 'Scheduled task updated successfully.');
$this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks');
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Team;
use Livewire\Component;
class Create extends Component
@@ -16,11 +17,7 @@ class Create extends Component
$this->limit_reached = false;
return;
}
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$this->limit_reached = $servers >= $serverLimit;
$this->limit_reached = Team::serverLimitReached();
}
public function render()
{

View File

@@ -64,7 +64,7 @@ class Form extends Component
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
$this->server->settings->save();
$this->dispatch('success', 'Server updated successfully.');
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -88,7 +88,7 @@ class Form extends Component
}
public function validateServer($install = true)
{
$this->dispatch('validateServer', $install);
$this->dispatch('init', $install);
}
public function submit()
@@ -113,6 +113,6 @@ class Form extends Component
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated successfully.');
$this->dispatch('success', 'Server updated.');
}
}

View File

@@ -60,7 +60,7 @@ class LogDrains extends Component
return;
}
$this->dispatch('serverRefresh');
$this->dispatch('success', 'Log drain service started successfully.');
$this->dispatch('success', 'Log drain service started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -126,7 +126,7 @@ class LogDrains extends Component
]);
}
$this->server->settings->save();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
return true;
} catch (\Throwable $e) {
if ($type === 'newrelic') {

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Server\New;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use App\Models\Team;
use Livewire\Component;
class ByIp extends Component
@@ -76,6 +77,9 @@ class ByIp extends Component
if (is_null($this->private_key_id)) {
return $this->dispatch('error', 'You must select a private key');
}
if (Team::serverLimitReached()) {
return $this->dispatch('error', 'You have reached the server limit for your subscription.');
}
$payload = [
'name' => $this->name,
'description' => $this->description,

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class DynamicConfigurationNavbar extends Component
{
public $server_id;
public $fileName = '';
public $value = '';
public $newFile = false;
public function delete(string $fileName)
{
$server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first();
$proxy_path = get_proxy_path();
$file = str_replace('|', '.', $fileName);
instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server);
$this->dispatch('success', 'File deleted.');
$this->dispatch('loadDynamicConfigurations');
$this->dispatch('refresh');
}
public function render()
{
return view('livewire.server.proxy.dynamic-configuration-navbar');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class DynamicConfigurations extends Component
{
public ?Server $server = null;
public $parameters = [];
public Collection $contents;
protected $listeners = ['loadDynamicConfigurations', 'refresh' => '$refresh'];
protected $rules = [
'contents.*' => 'nullable|string',
];
public function loadDynamicConfigurations()
{
$proxy_path = get_proxy_path();
$files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server);
$files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file));
$files = $files->map(fn ($file) => trim($file));
$files = $files->sort();
if ($files->contains('coolify.yaml')) {
$files = $files->filter(fn ($file) => $file !== 'coolify.yaml')->prepend('coolify.yaml');
}
$contents = collect([]);
foreach ($files as $file) {
$without_extension = str_replace('.', '|', $file);
$contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
}
$this->contents = $contents;
}
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.dynamic-configurations');
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Models\Server;
use Illuminate\Routing\Route;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
class NewDynamicConfiguration extends Component
{
public string $fileName = '';
public string $value = '';
public bool $newFile = false;
public Server $server;
public $server_id;
public $parameters = [];
public function mount()
{
$this->parameters = get_route_parameters();
if ($this->fileName !== '') {
$this->fileName = str_replace('|', '.', $this->fileName);
}
}
public function addDynamicConfiguration()
{
try {
$this->validate([
'fileName' => 'required',
'value' => 'required',
]);
if (data_get($this->parameters, 'server_uuid')) {
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
}
if (!is_null($this->server_id)) {
$this->server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first();
}
if (is_null($this->server)) {
return redirect()->route('server.index');
}
if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) {
$this->fileName = "{$this->fileName}.yaml";
}
if ($this->fileName === 'coolify.yaml') {
$this->dispatch('error', 'File name is reserved.');
return;
}
$proxy_path = get_proxy_path();
$file = "{$proxy_path}/dynamic/{$this->fileName}";
if ($this->newFile) {
$exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server);
if ($exists == 1) {
$this->dispatch('error', 'File already exists');
return;
}
}
$yaml = Yaml::parse($this->value);
$yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml;
$base64_value = base64_encode($this->value);
instant_remote_process(["echo '{$base64_value}' | base64 -d > {$file}"], $this->server);
$this->dispatch('loadDynamicConfigurations');
$this->dispatch('dynamic-configuration-added');
$this->dispatch('success', 'Dynamic configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.new-dynamic-configuration');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
@@ -21,7 +22,15 @@ class ValidateAndInstall extends Component
public $error = null;
public bool $ask = false;
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
protected $listeners = [
'init',
'validateConnection',
'validateOS',
'validateDockerEngine',
'validateDockerVersion',
'startProxy',
'refresh' => '$refresh',
];
public function init(bool $install = true)
{
@@ -35,31 +44,29 @@ class ValidateAndInstall extends Component
$this->error = null;
$this->number_of_tries = 0;
if (!$this->ask) {
$this->dispatch('validateServerNow');
$this->dispatch('validateConnection');
}
}
public function startValidatingAfterAsking() {
public function startValidatingAfterAsking()
{
$this->ask = false;
$this->init();
}
public function validateServer()
public function startProxy()
{
try {
$this->validateConnection();
$this->validateOS();
$this->validateDockerEngine();
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
$proxy = StartProxy::run($this->server, false);
if ($proxy === 'OK') {
$this->proxy_started = true;
} else {
throw new \Exception("Proxy could not be started.");
}
} else {
$proxy = StartProxy::run($this->server);
if ($proxy) {
$this->proxy_started = true;
}
$this->proxy_started = true;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -71,6 +78,7 @@ class ValidateAndInstall extends Component
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.';
return;
}
$this->dispatch('validateOS');
}
public function validateOS()
{
@@ -79,6 +87,7 @@ class ValidateAndInstall extends Component
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
$this->dispatch('validateDockerEngine');
}
public function validateDockerEngine()
{
@@ -90,29 +99,39 @@ class ValidateAndInstall extends Component
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
} else {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
if ($this->number_of_tries == 0) {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'init');
}
return;
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
} else {
$this->validateDockerVersion();
}
$this->dispatch('validateDockerVersion');
}
public function validateDockerVersion()
{
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated successfully.');
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
}
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
$this->dispatch('startProxy');
}
public function render()
{

View File

@@ -81,6 +81,6 @@ class Backup extends Component
}
public function submit()
{
$this->dispatch('success', 'Backup updated successfully.');
$this->dispatch('success', 'Backup updated.');
}
}

View File

@@ -49,7 +49,7 @@ class Email extends Component
'settings.smtp_from_name' => 'required',
]);
$this->settings->save();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -61,7 +61,7 @@ class Email extends Component
'settings.resend_api_key' => 'required'
]);
$this->settings->save();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
$this->settings->resend_enabled = false;
return handleError($e, $this);
@@ -98,7 +98,7 @@ class Email extends Component
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->save();
$this->dispatch('success', 'Settings saved successfully.');
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,8 +2,10 @@
namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Change extends Component
@@ -15,6 +17,7 @@ class Change extends Component
public ?bool $default_permissions = true;
public ?bool $preview_deployment_permissions = true;
public ?bool $administration = false;
public $parameters;
public ?GithubApp $github_app;
@@ -34,8 +37,52 @@ class Change extends Component
'github_app.client_secret' => 'required|string',
'github_app.webhook_secret' => 'required|string',
'github_app.is_system_wide' => 'required|bool',
'github_app.contents' => 'nullable|string',
'github_app.metadata' => 'nullable|string',
'github_app.pull_requests' => 'nullable|string',
'github_app.administration' => 'nullable|string',
];
public function checkPermissions()
{
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
}
// public function check()
// {
// Need administration:read:write permission
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository
// $github_access_token = generate_github_installation_token($this->github_app);
// $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
// $runners_by_repository = collect([]);
// $repositories = $repositories->json()['repositories'];
// foreach ($repositories as $repository) {
// $runners_downloads = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/downloads");
// $runners = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners");
// $token = Http::withHeaders([
// 'Authorization' => "Bearer $github_access_token",
// 'Accept' => 'application/vnd.github+json'
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/registration-token");
// $token = $token->json();
// $remove_token = Http::withHeaders([
// 'Authorization' => "Bearer $github_access_token",
// 'Accept' => 'application/vnd.github+json'
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/remove-token");
// $remove_token = $remove_token->json();
// $runners_by_repository->put($repository['full_name'], [
// 'token' => $token,
// 'remove_token' => $remove_token,
// 'runners' => $runners->json(),
// 'runners_downloads' => $runners_downloads->json()
// ]);
// }
// ray($runners_by_repository);
// }
public function mount()
{
$github_app_uuid = request()->github_app_uuid;
@@ -103,7 +150,7 @@ class Change extends Component
'github_app.is_system_wide' => 'required|bool',
]);
$this->github_app->save();
$this->dispatch('success', 'Github App updated successfully.');
$this->dispatch('success', 'Github App updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -111,6 +158,13 @@ class Change extends Component
public function instantSave()
{
try {
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->github_app->save();
$this->dispatch('success', 'Github App updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete()

View File

@@ -2,11 +2,18 @@
namespace App\Livewire\Subscription;
use App\Models\Team;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Actions extends Component
{
public $server_limits = 0;
public function mount()
{
$this->server_limits = Team::serverLimit();
}
public function cancel()
{
try {
@@ -69,7 +76,8 @@ class Actions extends Component
return handleError($e, $this);
}
}
public function stripeCustomerPortal() {
public function stripeCustomerPortal()
{
$session = getStripeCustomerPortalSession(currentTeam());
redirect($session->url);
}

View File

@@ -10,14 +10,19 @@ class Index extends Component
{
public InstanceSettings $settings;
public bool $alreadySubscribed = false;
public function mount() {
public function mount()
{
if (!isCloud()) {
return redirect(RouteServiceProvider::HOME);
}
if (data_get(currentTeam(), 'subscription')) {
return redirect()->route('subscription.show');
}
$this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
public function stripeCustomerPortal() {
public function stripeCustomerPortal()
{
$session = getStripeCustomerPortalSession(currentTeam());
if (is_null($session)) {
return;

View File

@@ -9,8 +9,9 @@ use Stripe\Checkout\Session;
class PricingPlans extends Component
{
public bool $isTrial = false;
public function mount() {
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
public function mount()
{
$this->isTrial = !data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
if (config('constants.limits.trial_period') == 0) {
$this->isTrial = false;
}
@@ -26,15 +27,15 @@ class PricingPlans extends Component
case 'basic-yearly':
$priceId = config('subscription.stripe_price_id_basic_yearly');
break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'pro-monthly':
$priceId = config('subscription.stripe_price_id_pro_monthly');
break;
case 'pro-yearly':
$priceId = config('subscription.stripe_price_id_pro_yearly');
break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'ultimate-yearly':
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
break;
@@ -64,18 +65,25 @@ class PricingPlans extends Component
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {
$payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
if (str($type)->contains('ultimate')) {
$payload['line_items'][0]['adjustable_quantity'] = [
'enabled' => true,
'minimum' => 10,
];
$payload['line_items'][0]['quantity'] = 10;
}
if (!data_get($team, 'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {
$payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
];
}
$payload['payment_method_collection'] = 'if_required';
}
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Subscription;
use Livewire\Component;
class Show extends Component
{
public function mount()
{
if (!isCloud()) {
return redirect()->route('dashboard');
}
if (!data_get(currentTeam(), 'subscription')) {
return redirect()->route('subscription.index');
}
}
public function render()
{
return view('livewire.subscription.show');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Livewire\Tags;
use App\Http\Controllers\Api\Deploy;
use App\Http\Controllers\Api\APIDeploy as Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Component;

View File

@@ -37,7 +37,7 @@ class Index extends Component
try {
$this->team->save();
refreshSession();
$this->dispatch('success', 'Team updated successfully.');
$this->dispatch('success', 'Team updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -79,7 +79,7 @@ class InviteLink extends Component
]);
$mail->subject('You have been invited to ' . currentTeam()->name . ' on ' . config('app.name') . '.');
send_user_an_email($mail, $this->email);
$this->dispatch('success', 'Invitation sent via email successfully.');
$this->dispatch('success', 'Invitation sent via email.');
$this->dispatch('refreshInvitations');
return;
} else {

View File

@@ -16,6 +16,11 @@ class Member extends Component
$this->dispatch('reloadWindow');
}
public function makeOwner()
{
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
$this->dispatch('reloadWindow');
}
public function makeReadonly()
{
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
@@ -26,7 +31,7 @@ class Member extends Component
{
$this->member->teams()->detach(currentTeam());
Cache::forget("team:{$this->member->id}");
Cache::remember('team:' . $this->member->id, 3600, function() {
Cache::remember('team:' . $this->member->id, 3600, function () {
return $this->member->teams()->first();
});
$this->dispatch('reloadWindow');

View File

@@ -6,7 +6,6 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
@@ -274,15 +273,11 @@ class Application extends BaseModel
foreach ($additional_servers_status as $status) {
$server_status = str($status)->before(':')->value();
$server_health = str($status)->after(':')->value() ?? 'unhealthy';
if ($server_status !== 'running') {
if ($main_server_status !== $server_status) {
$complex_status = 'degraded';
}
if ($main_server_status !== $server_status) {
$complex_status = 'degraded';
}
if ($server_health !== 'healthy') {
if ($main_server_health !== $server_health) {
$complex_health = 'unhealthy';
}
if ($main_server_health !== $server_health) {
$complex_health = 'unhealthy';
}
}
return "$complex_status:$complex_health";

View File

@@ -57,6 +57,7 @@ class LocalFileVolume extends BaseModel
if ($content) {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path");
$commands->push("chmod +x $path");
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");

View File

@@ -10,6 +10,7 @@ use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -69,7 +70,7 @@ class Server extends BaseModel
static public function isUsable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false);
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
}
static public function destinationsByServer(string $server_id)
@@ -146,11 +147,34 @@ class Server extends BaseModel
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
ray('skipping 1.2.3.4');
// ray('skipping 1.2.3.4');
return true;
}
if ($this->settings->force_disabled === true) {
// ray('force_disabled');
return true;
}
return false;
}
public function isForceDisabled()
{
return $this->settings->force_disabled;
}
public function forceEnableServer()
{
$this->settings->update([
'force_disabled' => false,
]);
}
public function forceDisableServer()
{
$this->settings->update([
'force_disabled' => true,
]);
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
public function isServerReady(int $tries = 3)
{
if ($this->skipServer()) {
@@ -374,7 +398,7 @@ class Server extends BaseModel
}
public function isFunctional()
{
return $this->settings->is_reachable && $this->settings->is_usable;
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
}
public function isLogDrainEnabled()
{
@@ -398,10 +422,10 @@ class Server extends BaseModel
}
});
if ($supported->count() === 1) {
ray('supported');
// ray('supported');
return str($supported->first());
} else {
ray('not supported');
// ray('not supported');
return false;
}
}
@@ -468,6 +492,16 @@ class Server extends BaseModel
}
return false;
}
try {
instant_remote_process(["docker version"], $this);
} catch (\Throwable $e) {
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
throw new \Exception('Server is not usable. Docker Engine is not running.');
}
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server);

View File

@@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class Service extends BaseModel
{
@@ -28,47 +27,73 @@ class Service extends BaseModel
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function status() {
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
public function status()
{
$applications = $this->applications;
$databases = $this->databases;
$complexStatus = null;
$complexHealth = null;
foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
$status = str($application->status)->before('(')->trim();
$health = str($application->status)->between('(', ')')->trim();
if ($complexStatus === 'degraded') {
continue;
}
if ($status->startsWith('running')) {
if ($complexStatus === 'exited') {
$complexStatus = 'degraded';
} else {
$complexStatus = 'running';
}
} else if ($status->startsWith('restarting')) {
$complexStatus = 'degraded';
} else if ($status->startsWith('exited')) {
$complexStatus = 'exited';
}
if ($health->value() === 'healthy') {
if ($complexHealth === 'unhealthy') {
continue;
}
$complexHealth = 'healthy';
} else {
$isDegraded = true;
$complexHealth = 'unhealthy';
}
}
foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
$status = str($database->status)->before('(')->trim();
$health = str($database->status)->between('(', ')')->trim();
if ($complexStatus === 'degraded') {
continue;
}
if ($status->startsWith('running')) {
if ($complexStatus === 'exited') {
$complexStatus = 'degraded';
} else {
$complexStatus = 'running';
}
} else if ($status->startsWith('restarting')) {
$complexStatus = 'degraded';
} else if ($status->startsWith('exited')) {
$complexStatus = 'exited';
}
if ($health->value() === 'healthy') {
if ($complexHealth === 'unhealthy') {
continue;
}
$complexHealth = 'healthy';
} else {
$isDegraded = true;
$complexHealth = 'unhealthy';
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
return "{$complexStatus}:{$complexHealth}";
}
public function extraFields()
{
@@ -414,7 +439,7 @@ class Service extends BaseModel
public function documentation()
{
$services = getServiceTemplates();
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
$service = data_get($services, str($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url'));
}
public function applications()

View File

@@ -30,8 +30,7 @@ class Subscription extends Model
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
}
if (isStripe()) {
} else if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'zero';
}
@@ -55,7 +54,7 @@ class Subscription extends Model
};
})->first();
if ($stripePlanId) {
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
return str($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'zero';

View File

@@ -48,7 +48,22 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
return explode(',', $recipients);
}
static public function serverLimitReached() {
$serverLimit = Team::serverLimit();
$team = currentTeam();
$servers = $team->servers->count();
return $servers >= $serverLimit;
}
public function serverOverflow() {
if ($this->serverLimit() < $this->servers->count()) {
return true;
}
return false;
}
static public function serverLimit()
{
return Team::find(currentTeam()->id)->limits['serverLimit'];
}
public function limits(): Attribute
{
return Attribute::make(
@@ -63,14 +78,19 @@ class Team extends Model implements SendsDiscord, SendsEmail
$subscription = $subscription->type();
}
}
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
if ($this->custom_server_limit) {
$serverLimit = $this->custom_server_limit;
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
}
);
}
public function environment_variables() {
public function environment_variables()
{
return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id');
}
public function members()
@@ -130,7 +150,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return $this->hasMany(S3Storage::class)->where('is_usable', true);
}
public function trialEnded() {
public function trialEnded()
{
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
@@ -138,7 +159,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
]);
}
}
public function trialEndedButSubscribed() {
public function trialEndedButSubscribed()
{
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,

View File

@@ -67,7 +67,7 @@ class User extends Authenticatable implements SendsEmail
'team_id' => session('currentTeam')->id
]);
return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
return new NewAccessToken($token, $token->getKey() . '|' . $plainTextToken);
}
public function teams()
{
@@ -103,9 +103,13 @@ class User extends Authenticatable implements SendsEmail
public function isAdmin()
{
return data_get($this->pivot, 'role') === 'admin' || data_get($this->pivot, 'role') === 'owner';
return $this->role() === 'admin' || $this->role() === 'owner';
}
public function isOwner()
{
return $this->role() === 'owner';
}
public function isAdminFromSession()
{
if (auth()->user()->id === 0) {
@@ -155,6 +159,9 @@ class User extends Authenticatable implements SendsEmail
public function role()
{
return session('currentTeam')->pivot->role;
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceDisabled extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
$mail->view('emails.server-force-disabled', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$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/subsciprtions).";
return $message;
}
public function toTelegram(): array
{
return [
"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/subsciprtions)."
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceEnabled extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
$mail->view('emails.server-force-enabled', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "Coolify: Server ({$this->server->name}) enabled again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server ({$this->server->name}) enabled again!"
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ResourceView extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public ?string $wire = null,
public ?string $logo = null,
public ?string $documentation = null,
public bool $upgrade = false,
)
{
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.resource-view');
}
}

View File

@@ -11,11 +11,11 @@ class Index extends Component
/**
* Create a new component instance.
*/
public function __construct(
public string $status = 'exited',
)
{
//
public $resource = null,
public bool $showRefreshButton = true,
) {
}
/**

View File

@@ -8,7 +8,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null)
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false)
{
$application_id = $application->id;
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
@@ -37,7 +37,8 @@ function queue_application_deployment(Application $application, string $deployme
'is_webhook' => $is_webhook,
'restart_only' => $restart_only,
'commit' => $commit,
'git_type' => $git_type
'git_type' => $git_type,
'only_this_server' => $only_this_server
]);
if ($no_questions_asked) {

View File

@@ -212,7 +212,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?string $service_name = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -264,6 +264,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
$http_label = "http-{$loop}-{$uuid}";
$https_label = "https-{$loop}-{$uuid}";
if ($service_name) {
$http_label = "http-{$loop}-{$uuid}-{$service_name}";
$https_label = "https-{$loop}-{$uuid}-{$service_name}";
}
if ($schema === 'https') {
// Set labels for https
@@ -419,7 +423,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--security-opt',
'--sysctl',
'--ulimit',
'--device'
'--device',
]);
$mapping = collect([
'--cap-add' => 'cap_add',
@@ -431,6 +435,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--init' => 'init',
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
'--ip' => 'ip',
]);
foreach ($matches as $match) {
$option = $match[1];

View File

@@ -86,3 +86,8 @@ function get_installation_path(GithubApp $source)
$installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps';
return "$github->html_url/$installation_path/$name/installations/new";
}
function get_permissions_path(GithubApp $source) {
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = Str::of(Str::kebab($github->name));
return "$github->html_url/settings/apps/$name/permissions";
}

View File

@@ -140,7 +140,9 @@ function generate_default_proxy_configuration(Server $server)
"--entrypoints.http.address=:80",
"--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true",
"--entryPoints.http.http2.maxConcurrentStreams=50",
"--entrypoints.https.http.encodequerysemicolons=true",
"--entryPoints.https.http2.maxConcurrentStreams=50",
"--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true",

View File

@@ -110,6 +110,9 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
}
function generateSshCommand(Server $server, string $command)
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$user = $server->user;
$port = $server->port;
$privateKeyLocation = savePrivateKeyToFs($server);

View File

@@ -4,8 +4,8 @@ use App\Models\EnvironmentVariable;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
function replaceRegex(?string $name = null)
@@ -95,12 +95,17 @@ function updateCompose($resource)
// Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($resource->fqdn);
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) {
$generatedEnv->value = $resource->fqdn;
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($resource->fqdn);
$url = $url->getHost();
ray($url);
if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://');
$generatedEnv->value = $url;

View File

@@ -104,13 +104,13 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests.", "Please try again in {$error->secondsUntilAvailable} seconds.");
return $livewire->dispatch('error', 'Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.');
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Duplicate entry found.", "Please use a different name.");
return $livewire->dispatch('error', 'Duplicate entry found. Please use a different name.');
}
return "Duplicate entry found. Please use a different name.";
}
@@ -125,9 +125,6 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
}
if (isset($livewire)) {
if (str($message)->length() > 20) {
return $livewire->dispatch('error', 'Error occured', $message);
}
return $livewire->dispatch('error', $message);
}
throw new Exception($message);
@@ -975,7 +972,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
@@ -1039,7 +1036,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled()));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), service_name: $serviceName));
}
}
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@@ -1480,7 +1477,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $preview_fqdn;
});
}
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns,serviceLabels: $serviceLabels));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns, serviceLabels: $serviceLabels));
}
}
}

View File

@@ -109,7 +109,7 @@ function isPaddle()
function getStripeCustomerPortalSession(Team $team)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.index');
$return_url = route('subscription.show');
$stripe_customer_id = data_get($team,'subscription.stripe_customer_id');
if (!$stripe_customer_id) {
return null;
@@ -123,7 +123,7 @@ function getStripeCustomerPortalSession(Team $team)
function allowedPathsForUnsubscribedAccounts()
{
return [
'subscription',
'subscription/new',
'login',
'logout',
'waitlist',

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.221',
'release' => '4.0.0-beta.226',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -13,6 +13,12 @@ return [
'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null),
'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null),
'stripe_price_id_basic_monthly_old' => env('STRIPE_PRICE_ID_BASIC_MONTHLY_OLD', null),
'stripe_price_id_basic_yearly_old' => env('STRIPE_PRICE_ID_BASIC_YEARLY_OLD', null),
'stripe_price_id_pro_monthly_old' => env('STRIPE_PRICE_ID_PRO_MONTHLY_OLD', null),
'stripe_price_id_pro_yearly_old' => env('STRIPE_PRICE_ID_PRO_YEARLY_OLD', null),
'stripe_price_id_ultimate_monthly_old' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD', null),
'stripe_price_id_ultimate_yearly_old' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD', null),
// Paddle
'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.221';
return '4.0.0-beta.226';

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('github_apps', function (Blueprint $table) {
$table->string('contents')->nullable();
$table->string('metadata')->nullable();
$table->string('pull_requests')->nullable();
$table->string('administration')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('github_apps', function (Blueprint $table) {
$table->dropColumn('contents');
$table->dropColumn('metadata');
$table->dropColumn('pull_requests');
$table->dropColumn('administration');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->boolean('only_this_server')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('only_this_server');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->integer('custom_server_limit')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('custom_server_limit');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('force_disabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('force_disabled');
});
}
};

View File

@@ -57,6 +57,12 @@ services:
- STRIPE_PRICE_ID_PRO_YEARLY
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY
- STRIPE_PRICE_ID_ULTIMATE_YEARLY
- STRIPE_PRICE_ID_BASIC_MONTHLY_OLD
- STRIPE_PRICE_ID_BASIC_YEARLY_OLD
- STRIPE_PRICE_ID_PRO_MONTHLY_OLD
- STRIPE_PRICE_ID_PRO_YEARLY_OLD
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD
- STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD
- STRIPE_EXCLUDED_PLANS
- PADDLE_VENDOR_ID
- PADDLE_WEBHOOK_SECRET

19
public/svgs/appsmith.svg Normal file
View File

@@ -0,0 +1,19 @@
<svg width="924" height="235" viewBox="0 0 924 235" fill="ffffff" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_41)">
<path d="M602.936 59.0637C601.82 58.0005 600.937 56.7175 600.342 55.2956C599.746 53.8738 599.452 52.344 599.478 50.8028C599.478 47.673 600.722 44.6712 602.935 42.4581C605.148 40.2449 608.15 39.0016 611.28 39.0016C612.821 38.9758 614.351 39.2697 615.772 39.8648C617.194 40.46 618.477 41.3434 619.54 42.4593C620.635 43.5541 621.498 44.8581 622.078 46.2936C622.658 47.7291 622.943 49.2667 622.916 50.8146C622.941 52.3558 622.648 53.8856 622.052 55.3074C621.457 56.7293 620.574 58.0123 619.458 59.0755C618.389 60.1594 617.114 61.0169 615.706 61.5968C614.299 62.1767 612.79 62.4671 611.268 62.4507C609.723 62.4725 608.19 62.184 606.758 61.6022C605.327 61.0204 604.027 60.1572 602.936 59.0637V59.0637Z" fill="#ffffff"/>
<path d="M62.7915 160.047C61.7766 158.246 60.9899 154.705 60.4313 149.426C54.5778 157.876 45.2351 162.097 32.4032 162.089C22.8284 162.089 15.2009 159.784 9.52053 155.173C3.84018 150.563 1 144.143 1 135.914C1 120.045 12.1443 110.997 34.433 108.771L47.6033 107.591C51.9934 107.036 55.1443 105.986 57.0443 104.475C58.023 103.637 58.7921 102.581 59.29 101.393C59.788 100.204 60.001 98.9159 59.912 97.6303C59.912 93.9169 58.7043 91.1868 56.289 89.4402C53.8737 87.6936 49.7904 86.8204 44.0393 86.8204C37.8554 86.8204 33.4103 87.8628 30.7038 89.9477C28.0013 92.0365 26.42 95.6005 25.9833 100.675H5.03605C6.27125 80.8646 19.3313 70.9555 44.2163 70.9476C68.4247 70.9476 80.5249 79.6727 80.5171 97.1229V143.561C80.5171 151.224 81.6972 156.731 84.0574 160.083L62.7915 160.047ZM53.6691 141.425C57.8389 137.766 59.9199 132.503 59.912 125.635V117.704C57.894 119.51 54.566 120.69 49.9517 121.245L38.4691 122.543C32.8045 123.227 28.792 124.553 26.4318 126.52C25.2054 127.563 24.2386 128.876 23.6077 130.357C22.9769 131.838 22.6993 133.446 22.797 135.052C22.7027 136.7 22.9938 138.348 23.6473 139.864C24.3008 141.379 25.2986 142.722 26.5616 143.785C29.1028 145.862 32.7337 146.901 37.4542 146.901C44.1022 146.877 49.5072 145.052 53.6691 141.425Z" fill="#ffffff"/>
<path d="M186.056 83.2092C193.042 91.5409 196.531 102.63 196.524 116.477C196.516 130.324 193.026 141.338 186.056 149.521C178.975 157.852 169.463 162.018 157.52 162.018C145.577 162.018 136.569 157.573 130.495 148.683V193.091H109.253V72.9067H129.681V85.4042C135.975 75.7272 145.263 70.8847 157.544 70.8768C169.463 70.8768 178.967 74.9876 186.056 83.2092V83.2092ZM152.458 144.505C159.428 144.505 164.935 141.972 168.979 136.905C173.023 131.838 174.99 124.805 174.88 115.804C174.88 106.906 172.937 100.12 169.05 95.4471C165.164 90.7738 159.621 88.4371 152.422 88.4371C145.341 88.4371 139.834 90.8603 135.9 95.7067C131.967 100.553 130 107.477 130 116.477C130 125.588 132.026 132.511 136.077 137.247C139.901 142.086 145.361 144.505 152.458 144.505Z" fill="#ffffff"/>
<path d="M295.501 83.2092C302.487 91.5409 305.977 102.63 305.969 116.477C305.961 130.324 302.472 141.338 295.501 149.521C288.42 157.852 278.908 162.018 266.965 162.018C255.023 162.018 246.014 157.573 239.941 148.683V193.091H218.698V72.9067H239.138V85.4042C245.432 75.7272 254.72 70.8847 267.001 70.8768C278.873 70.8768 288.373 74.9876 295.501 83.2092ZM261.903 144.505C268.873 144.505 274.381 141.972 278.424 136.905C282.468 131.838 284.435 124.805 284.325 115.804C284.325 106.906 282.382 100.12 278.495 95.4471C274.609 90.7738 269.066 88.4371 261.867 88.4371C254.787 88.4371 249.279 90.8603 245.346 95.7067C241.412 100.553 239.445 107.477 239.445 116.477C239.445 125.588 241.475 132.511 245.534 137.247C249.311 142.086 254.747 144.505 261.844 144.505H261.903Z" fill="#ffffff"/>
<path d="M325.158 132.35H345.928C346.494 137.755 348.268 141.582 351.25 143.832C354.232 146.082 358.933 147.208 365.353 147.208C376.839 147.208 382.583 143.718 382.583 136.74C382.644 135.312 382.333 133.892 381.682 132.62C381.03 131.348 380.06 130.267 378.865 129.482C376.387 127.79 371.938 126.382 365.518 125.257L355.558 123.57C336.534 120.423 327.022 111.642 327.022 97.2291C327.022 89.0075 330.169 82.5601 336.463 77.8868C342.757 73.2135 351.651 70.8768 363.146 70.8768C388.023 70.8768 400.859 80.7112 401.653 100.38H381.591C381.363 95.2111 379.731 91.5251 376.694 89.3222C373.657 87.1193 369.153 86.0218 363.181 86.0297C353.048 86.0297 347.981 89.4088 347.981 96.167C347.928 97.4972 348.209 98.8196 348.799 100.013C349.388 101.207 350.268 102.234 351.356 103C353.614 104.628 357.332 105.895 362.509 106.8L373.484 108.488C384.066 110.399 391.748 113.44 396.532 117.61C401.315 121.78 403.707 127.578 403.707 135.005C403.707 143.785 400.332 150.539 393.581 155.268C386.831 159.996 377.315 162.356 365.034 162.349C339.591 162.388 326.298 152.388 325.158 132.35V132.35Z" fill="#ffffff"/>
<path d="M549.335 79.3265C554.677 84.9597 557.352 92.6699 557.36 102.457V160.047H536.117V105.832C536.117 94.8177 531.106 89.3104 521.082 89.3104C518.592 89.2151 516.113 89.6885 513.833 90.6945C511.553 91.7006 509.532 93.2129 507.924 95.1167C504.612 98.9954 502.952 104.652 502.944 112.087V160.047H481.867V105.832C481.867 94.8177 476.804 89.3104 466.678 89.3104C464.224 89.2393 461.785 89.7271 459.546 90.7371C457.307 91.7471 455.327 93.2527 453.756 95.1403C450.428 99.0189 448.768 104.676 448.776 112.111V160.071H427.534V72.9067H448.139V85.4042C454.094 75.7272 462.355 70.8847 472.921 70.8768C486.21 70.8768 495.218 76.2818 499.946 87.0918C506.468 76.2818 515.473 70.8768 526.959 70.8768C536.534 70.8768 543.993 73.6934 549.335 79.3265V79.3265Z" fill="#ffffff"/>
<path d="M720.347 89.1216H693.204V136.232C693.204 139.041 693.963 141.008 695.482 142.133C697 143.258 699.545 143.809 703.117 143.785H718.66V160C714.045 160.449 702.055 160.673 699.235 160.673C689.794 160.673 682.898 158.899 678.547 155.35C674.196 151.802 672.029 146.145 672.044 138.38V89.1216H652.289V72.9066H672.044V43.8165H693.204V72.9066H720.347V89.1216Z" fill="#ffffff"/>
<path d="M815.536 79.0669C821.004 84.5348 823.734 92.2765 823.726 102.292V160.047H802.484V105.832C802.484 94.8177 797.024 89.3104 786.104 89.3104C783.52 89.2569 780.955 89.746 778.572 90.7461C776.19 91.7463 774.044 93.2352 772.273 95.1167C768.512 98.9953 766.628 104.369 766.62 111.237V160.047H745.378V43.8165H766.62V84.5545C773.26 75.4439 782.095 70.8847 793.126 70.8768C802.614 70.8768 810.084 73.6069 815.536 79.0669V79.0669Z" fill="#ffffff"/>
<path d="M840.909 188.748V172.533H923.99V188.748H840.909Z" fill="#FF6D2D"/>
<path d="M622.101 143.832V72.9066H577.363V89.1216H600.836V143.832H577.363V160.047H645.574V143.832H622.101Z" fill="#ffffff"/>
</g>
<defs>
<clipPath id="clip0_1_41">
<rect width="924" height="235" fill="black"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

1
public/svgs/appwrite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="112" height="98" fill="none" class="u-max-width-100-percent" viewBox="0 0 112 98"><path fill="#FD366E" d="M111.1 73.473v24.49H48.87c-18.13 0-33.96-9.849-42.429-24.49A48.7 48.7 0 0 1 0 52.293V45.67a48.5 48.5 0 0 1 1.732-10.048C7.545 15.064 26.448 0 48.871 0c22.422 0 41.323 15.064 47.136 35.623H69.398C65.03 28.922 57.47 24.491 48.872 24.491s-16.16 4.43-20.528 11.132a24.3 24.3 0 0 0-3.042 6.68 24.5 24.5 0 0 0-.921 6.679c0 7.02 2.952 13.348 7.685 17.811a24.4 24.4 0 0 0 16.806 6.68z"/><path fill="#FD366E" d="M111.1 42.303v24.49H65.676a24.4 24.4 0 0 0 7.686-17.81c0-2.316-.321-4.556-.922-6.68z"/></svg>

After

Width:  |  Height:  |  Size: 649 B

BIN
public/svgs/babybuddy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

59
public/svgs/bitwarden.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
xml:space="preserve"
width="64"
height="64"
viewBox="0 0 64 63.999999"
sodipodi:docname="bitwarden-icon.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18"><path
d="M 0,1500 H 1500 V 0 H 0 Z"
id="path16"
inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1440"
inkscape:window-height="799"
id="namedview4"
showgrid="false"
fit-margin-top="50"
fit-margin-left="50"
fit-margin-right="50"
fit-margin-bottom="50"
inkscape:zoom="5.3400704"
inkscape:cx="72.883247"
inkscape:cy="23.722997"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1"
inkscape:current-layer="g10" /><g
id="g10"
inkscape:groupmode="layer"
inkscape:label="logo-vertical"
transform="matrix(1.3333333,0,0,-1.3333333,31.882666,774.35078)"><path
d="m 20.088053,578.76279 v -24.00016 c 0,-1.79111 -0.34893,-3.56734 -1.046785,-5.32749 -0.698165,-1.76104 -1.562895,-3.32302 -2.593423,-4.68822 -1.031908,-1.36442 -2.261363,-2.69188 -3.687599,-3.98376 -1.427771,-1.29219 -2.745111,-2.36474 -3.9537075,-3.21903 -1.2081373,-0.85369 -2.4685751,-1.66122 -3.7808519,-2.42195 -1.3125864,-0.76028 -2.2452616,-1.27562 -2.7971051,-1.54665 -0.5518437,-0.27117 -0.9947907,-0.47961 -1.3280751,-0.6253 -0.2500014,-0.12451 -0.5211689,-0.18713 -0.8124295,-0.18713 -0.2917201,0 -0.5624267,0.0627 -0.81258076,0.18713 -0.33343844,0.14566 -0.77607884,0.35413 -1.32776984,0.6253 -0.552611,0.27103 -1.4845188,0.78637 -2.7972576,1.54665 -1.3125851,0.76073 -2.5731749,1.56826 -3.7814668,2.42195 -1.2081351,0.85429 -2.5257829,1.92684 -3.9529389,3.21903 -1.427156,1.29188 -2.655998,2.61934 -3.687139,3.98376 -1.031297,1.3652 -1.896026,2.92718 -2.594036,4.68822 -0.69801,1.76015 -1.046939,3.53638 -1.046939,5.32749 v 24.00016 c 0,0.54172 0.198161,1.01088 0.594021,1.40629 0.395556,0.39586 0.864578,0.59402 1.405992,0.59402 h 36.000846 c 0.540954,0 1.009208,-0.19816 1.405377,-0.59402 0.396014,-0.39541 0.593868,-0.86457 0.593868,-1.40629 m -6.000346,-24.00016 v 20.00043 H 0.0880769 v -35.53105 c 2.4786974,1.31259 4.6980372,2.73943 6.6564844,4.281 4.8952787,3.83317 7.3431457,7.58318 7.3431457,11.24962"
style="fill:#3c8dbc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.15337521"
id="path28"
inkscape:connector-curvature="0" /></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,15 @@
<svg width="45" height="31" viewBox="0 0 45 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5345_61190)">
<path d="M42.9532 13.469C42.0762 13.469 41.4914 12.9612 41.4914 11.919V5.93276C41.4914 2.1112 39.8966 0 35.7767 0H33.8629V4.03534H34.4477C36.069 4.03534 36.8399 4.91724 36.8399 6.49396V11.7854C36.8399 14.0836 37.5309 15.019 39.046 15.5C37.5309 15.9544 36.8399 16.9164 36.8399 19.2146C36.8399 20.5242 36.8399 21.8336 36.8399 23.1432C36.8399 24.2388 36.8399 25.3078 36.5475 26.4034C36.2552 27.419 35.7767 28.381 35.1122 29.2094C34.7401 29.6906 34.3148 30.0914 33.8364 30.4656V31H35.7501C39.87 31 41.4649 28.8888 41.4649 25.0672V19.081C41.4649 18.012 42.023 17.531 42.9267 17.531H44.0165V13.4956H42.9532V13.469Z" fill="white"/>
<path d="M29.929 6.09391H24.0282C23.8953 6.09391 23.7891 5.98701 23.7891 5.85339V5.39909C23.7891 5.26547 23.8953 5.15857 24.0282 5.15857H29.9556C30.0884 5.15857 30.1948 5.26547 30.1948 5.39909V5.85339C30.1948 5.98701 30.0618 6.09391 29.929 6.09391Z" fill="white"/>
<path d="M30.9388 11.8661H26.6328C26.4999 11.8661 26.3936 11.7591 26.3936 11.6255V11.1713C26.3936 11.0377 26.4999 10.9307 26.6328 10.9307H30.9388C31.0717 10.9307 31.1779 11.0377 31.1779 11.1713V11.6255C31.1779 11.7325 31.0717 11.8661 30.9388 11.8661Z" fill="white"/>
<path d="M32.6401 8.97996H24.0282C23.8953 8.97996 23.7891 8.87306 23.7891 8.73944V8.28513C23.7891 8.1515 23.8953 8.04462 24.0282 8.04462H32.6135C32.7464 8.04462 32.8528 8.1515 32.8528 8.28513V8.73944C32.8528 8.84634 32.773 8.97996 32.6401 8.97996Z" fill="white"/>
<path d="M17.1972 7.40258C17.7819 7.40258 18.3668 7.45604 18.9249 7.58966V6.49396C18.9249 4.94396 19.7223 4.03534 21.3171 4.03534H21.9019V0H19.9881C15.8682 0 14.2734 2.1112 14.2734 5.93276V7.91034C15.2037 7.58966 16.1872 7.40258 17.1972 7.40258Z" fill="white"/>
<path d="M34.4476 21.94C34.0223 18.546 31.4175 15.7132 28.0684 15.0718C27.1381 14.8848 26.2078 14.858 25.3041 15.0184C25.2775 15.0184 25.2775 14.9916 25.2509 14.9916C23.789 11.9184 20.6527 9.88733 17.2504 9.88733C13.8481 9.88733 10.7383 11.865 9.24981 14.9382C9.22324 14.9382 9.22324 14.965 9.19665 14.965C8.23978 14.858 7.2829 14.9114 6.32602 15.152C3.03011 15.9537 0.531599 18.733 0.0797398 22.1002C0.0265799 22.4476 0 22.795 0 23.1158C0 24.1312 0.691078 25.0666 1.70111 25.2002C2.95037 25.3874 4.04014 24.4252 4.01357 23.196C4.01357 23.0088 4.01357 22.795 4.04014 22.608C4.25279 20.8976 5.5552 19.4546 7.25631 19.0537C7.78792 18.92 8.31951 18.8934 8.82454 18.9736C10.4459 19.1874 12.0407 18.3588 12.7318 16.9158C13.2368 15.8468 14.0342 14.9114 15.0974 14.4037C16.2669 13.8425 17.5959 13.7624 18.8186 14.19C20.0944 14.6442 21.0513 15.6062 21.6361 16.8088C22.2474 17.9848 22.5397 18.8132 23.8422 18.9736C24.3738 19.0537 25.8622 19.027 26.4205 19.0002C27.5102 19.0002 28.6 19.3744 29.3708 20.1494C29.8758 20.6838 30.2479 21.352 30.4074 22.1002C30.6466 23.3028 30.3543 24.5054 29.6366 25.414C29.1316 26.0554 28.4405 26.5364 27.6697 26.7502C27.2975 26.8572 26.9254 26.8838 26.5533 26.8838C26.3407 26.8838 26.0484 26.8838 25.7028 26.8838C24.6396 26.8838 22.3803 26.8838 20.6791 26.8838C19.5097 26.8838 18.5793 25.9486 18.5793 24.7726V20.8175V16.9425C18.5793 16.6218 18.3136 16.3546 17.9946 16.3546H17.1706C15.5492 16.3812 14.2468 18.1986 14.2468 20.1226C14.2468 22.0468 14.2468 27.1512 14.2468 27.1512C14.2468 29.2356 15.9213 30.9192 17.9946 30.9192C17.9946 30.9192 27.2179 30.8925 27.3507 30.8925C29.4771 30.6787 31.444 29.583 32.773 27.8994C34.1021 26.2692 34.7134 24.1312 34.4476 21.94Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5345_61190">
<rect width="44.2857" height="31" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

31
public/svgs/directus.svg Normal file
View File

@@ -0,0 +1,31 @@
<svg width="1024" height="1028" viewBox="0 0 1024 1028" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1255_4057)">
<rect width="1024" height="1028" fill="#6644FF"/>
<path d="M0 856.177V0H1024V1028H987.478C944.858 1005.08 881.857 978.413 792.583 956.099C706.535 934.591 579.091 924.288 443.621 913.336C289.815 900.902 125.662 887.631 0 856.177Z" fill="#754DFC"/>
<path d="M0 630.724V0H1024V864.167C987.48 836.871 914.997 793.6 788.518 757.39C703.262 732.982 576.241 718.365 441.22 702.828C288.002 685.197 124.482 666.38 0 630.724Z" fill="#8555F8"/>
<path d="M0 385.079V0H1024V661.623C992.243 634.157 920.217 583.508 783.219 539.058C698.867 511.689 572.433 492.648 438.037 472.408C285.691 449.465 123.114 424.981 0 385.079Z" fill="#945EF5"/>
<path d="M1024 0V470.993C995.948 444.51 924.692 389.046 780.011 339.296C696.15 310.459 570.068 289.215 436.045 266.632C284.289 241.062 122.352 213.776 0 171.81V0H1024Z" fill="#A366F1"/>
<path d="M170.96 0H1024V278.073C1008.47 260.248 941.186 192.346 771.386 130.624C688.04 100.328 562.349 76.8867 428.741 51.9689C343.042 35.9861 254.087 19.396 170.96 0Z" fill="#B36EEE"/>
<g filter="url(#filter0_dd_1255_4057)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M751.713 578.4C748 577.472 744.906 576.544 742.122 575.306C740.063 574.391 738.343 573.308 736.836 572.055C735.835 571.222 735.405 569.911 735.529 568.615C737.022 553.073 735.38 539.364 736.862 523.951C743.05 461.46 782.345 481.259 817.617 471.05C837.872 465.345 858.126 454.117 865.527 432.032C866.739 428.417 865.672 424.486 863.154 421.622C840.049 395.355 814.469 372.145 786.676 352.254C693.499 285.923 572.404 258.379 461.196 274.491C456.983 275.101 454.775 279.768 457.077 283.348C471.161 305.263 489.743 323.197 511.052 336.397C514.925 338.796 513.368 343.978 508.923 342.981C498.473 340.639 485.045 336.07 472.417 327.159C471.2 326.301 469.632 326.084 468.249 326.636C462.603 328.887 454.451 332.133 447.714 335.041C443.836 336.715 443.046 341.715 446.238 344.482C502.095 392.893 583.352 400.252 646.98 361.511C650.856 359.151 657.062 364.001 655.813 368.363C653.814 375.352 651.478 384.958 648.991 398.04C633.211 477.856 587.728 471.669 531.417 451.56C418.895 410.784 355.06 445.604 298.306 376.675C294.363 371.885 287.431 370.225 282.74 374.285C271.024 384.427 264.09 399.231 264.09 415.055C264.09 433.797 273.766 449.851 288.108 459.461C289.902 460.664 292.286 460.156 293.624 458.461C297.119 454.039 299.978 451.107 303.538 449.253C307.434 447.224 309.335 452.797 306.045 455.709C293.991 466.38 290.532 479.092 282.655 504.152C270.278 543.441 275.538 583.659 217.679 594.177C187.048 595.724 187.667 616.452 176.528 647.388C163.601 684.728 146.674 701.267 115.345 733.897C111.061 738.359 110.693 745.495 115.402 749.508C127.916 760.172 140.822 760.758 153.942 755.357C186.429 741.745 211.491 699.671 235.006 672.447C261.306 642.129 324.424 655.122 372.073 625.423C397.77 609.668 413.21 589.543 408.273 559.396C407.477 554.54 413.034 551.622 415.049 556.111C418.875 564.633 421.383 573.723 422.426 583.116C422.699 585.578 424.9 587.4 427.374 587.261C478.926 584.363 545.591 641.216 607.891 656.599C611.681 657.535 614.375 653.158 612.232 649.896C608.291 643.894 604.94 637.666 602.271 631.301C599.515 624.689 597.427 618.274 595.951 612.082C594.796 607.24 601.872 605.938 604.288 610.292C620.266 639.089 652.198 666.13 696.639 669.353C711.8 670.591 728.508 668.734 745.835 663.475C766.565 657.288 785.748 649.244 808.644 653.576C825.661 656.669 841.441 665.331 851.342 679.872C865.235 700.13 894.563 705.498 910.194 684.325C912.321 681.444 912.5 677.592 911.094 674.298C876.675 593.634 789.279 588.093 751.713 578.4Z" fill="white"/>
</g>
</g>
<defs>
<filter id="filter0_dd_1255_4057" x="52" y="222" width="920" height="608.703" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="12"/>
<feGaussianBlur stdDeviation="30"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0901961 0 0 0 0 0.160784 0 0 0 0 0.25098 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1255_4057"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="16"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0901961 0 0 0 0 0.160784 0 0 0 0 0.25098 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_1255_4057" result="effect2_dropShadow_1255_4057"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1255_4057" result="shape"/>
</filter>
<clipPath id="clip0_1255_4057">
<rect width="1024" height="1028" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

1
public/svgs/docker.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
public/svgs/dokuwiki.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/svgs/duplicati.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/svgs/emby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Some files were not shown because too many files have changed in this diff Show More