Merge branch 'next' into feat/prioritize-branch-selection
This commit is contained in:
commit
67c87324e5
118 changed files with 4563 additions and 832 deletions
8
.github/workflows/coolify-helper-next.yml
vendored
8
.github/workflows/coolify-helper-next.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -86,8 +86,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-helper.yml
vendored
8
.github/workflows/coolify-helper.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -85,8 +85,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -91,8 +91,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-realtime-next.yml
vendored
8
.github/workflows/coolify-realtime-next.yml
vendored
|
|
@ -48,8 +48,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -90,8 +90,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-realtime.yml
vendored
8
.github/workflows/coolify-realtime.yml
vendored
|
|
@ -48,8 +48,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -90,8 +90,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-staging-build.yml
vendored
8
.github/workflows/coolify-staging-build.yml
vendored
|
|
@ -64,8 +64,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
|
|
@ -110,8 +110,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
|
|
|
|||
8
.github/workflows/coolify-testing-host.yml
vendored
8
.github/workflows/coolify-testing-host.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
|
|
@ -81,8 +81,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -5389,7 +5389,6 @@ ### 🚀 Features
|
|||
- Add static ipv4 ipv6 support
|
||||
- Server disabled by overflow
|
||||
- Preview deployment logs
|
||||
- Collect webhooks during maintenance
|
||||
- Logs and execute commands with several servers
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
|
|
|||
|
|
@ -461,9 +461,10 @@ private function aggregateApplicationStatus($application, Collection $containerS
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount);
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount, preserveRestarting: true);
|
||||
}
|
||||
|
||||
private function aggregateServiceContainerStatuses($services)
|
||||
|
|
@ -518,8 +519,9 @@ private function aggregateServiceContainerStatuses($services)
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ class CleanupDocker
|
|||
|
||||
public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$realtimeImage = config('constants.coolify.realtime_image');
|
||||
$realtimeImageVersion = config('constants.coolify.realtime_version');
|
||||
$realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion";
|
||||
|
|
@ -26,9 +25,25 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
||||
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
|
||||
|
||||
$cleanupLog = [];
|
||||
|
||||
// Get all application image repositories to exclude from prune
|
||||
$applications = $server->applications();
|
||||
$applicationImageRepos = collect($applications)->map(function ($app) {
|
||||
return $app->docker_registry_image_name ?? $app->uuid;
|
||||
})->unique()->values();
|
||||
|
||||
// Clean up old application images while preserving N most recent for rollback
|
||||
$applicationCleanupLog = $this->cleanupApplicationImages($server, $applications);
|
||||
$cleanupLog = array_merge($cleanupLog, $applicationCleanupLog);
|
||||
|
||||
// Build image prune command that excludes application images
|
||||
// This ensures we clean up non-Coolify images while preserving rollback images
|
||||
$imagePruneCmd = $this->buildImagePruneCommand($applicationImageRepos);
|
||||
|
||||
$commands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
|
||||
'docker image prune -af --filter "label!=coolify.managed=true"',
|
||||
$imagePruneCmd,
|
||||
'docker builder prune -af',
|
||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
"docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
|
|
@ -44,7 +59,6 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$commands[] = 'docker network prune -f';
|
||||
}
|
||||
|
||||
$cleanupLog = [];
|
||||
foreach ($commands as $command) {
|
||||
$commandOutput = instant_remote_process([$command], $server, false);
|
||||
if ($commandOutput !== null) {
|
||||
|
|
@ -57,4 +71,122 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
|
||||
return $cleanupLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a docker image prune command that excludes application image repositories.
|
||||
*
|
||||
* Since docker image prune doesn't support excluding by repository name directly,
|
||||
* we use a shell script approach to delete unused images while preserving application images.
|
||||
*/
|
||||
private function buildImagePruneCommand($applicationImageRepos): string
|
||||
{
|
||||
// Step 1: Always prune dangling images (untagged)
|
||||
$commands = ['docker image prune -f'];
|
||||
|
||||
if ($applicationImageRepos->isEmpty()) {
|
||||
// No applications, add original prune command for all unused images
|
||||
$commands[] = 'docker image prune -af --filter "label!=coolify.managed=true"';
|
||||
} else {
|
||||
// Build grep pattern to exclude application image repositories
|
||||
$excludePatterns = $applicationImageRepos->map(function ($repo) {
|
||||
// Escape special characters for grep extended regex (ERE)
|
||||
// ERE special chars: . \ + * ? [ ^ ] $ ( ) { } |
|
||||
return preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $repo);
|
||||
})->implode('|');
|
||||
|
||||
// Delete unused images that:
|
||||
// - Are not application images (don't match app repos)
|
||||
// - Don't have coolify.managed=true label
|
||||
// Images in use by containers will fail silently with docker rmi
|
||||
// Pattern matches both uuid:tag and uuid_servicename:tag (Docker Compose with build)
|
||||
$commands[] = "docker images --format '{{.Repository}}:{{.Tag}}' | ".
|
||||
"grep -v -E '^({$excludePatterns})[_:].+' | ".
|
||||
"grep -v '<none>' | ".
|
||||
"xargs -r -I {} sh -c 'docker inspect --format \"{{{{index .Config.Labels \\\"coolify.managed\\\"}}}}\" \"{}\" 2>/dev/null | grep -q true || docker rmi \"{}\" 2>/dev/null' || true";
|
||||
}
|
||||
|
||||
return implode(' && ', $commands);
|
||||
}
|
||||
|
||||
private function cleanupApplicationImages(Server $server, $applications = null): array
|
||||
{
|
||||
$cleanupLog = [];
|
||||
|
||||
if ($applications === null) {
|
||||
$applications = $server->applications();
|
||||
}
|
||||
|
||||
$disableRetention = $server->settings->disable_application_image_retention ?? false;
|
||||
|
||||
foreach ($applications as $application) {
|
||||
$imagesToKeep = $disableRetention ? 0 : ($application->settings->docker_images_to_keep ?? 2);
|
||||
$imageRepository = $application->docker_registry_image_name ?? $application->uuid;
|
||||
|
||||
// Get the currently running image tag
|
||||
$currentTagCommand = "docker inspect --format='{{.Config.Image}}' {$application->uuid} 2>/dev/null | grep -oP '(?<=:)[^:]+$' || true";
|
||||
$currentTag = instant_remote_process([$currentTagCommand], $server, false);
|
||||
$currentTag = trim($currentTag ?? '');
|
||||
|
||||
// List all images for this application with their creation timestamps
|
||||
// Use wildcard to match both uuid:tag and uuid_servicename:tag (Docker Compose with build)
|
||||
$listCommand = "docker images --format '{{.Repository}}:{{.Tag}}#{{.CreatedAt}}' --filter reference='{$imageRepository}*' 2>/dev/null || true";
|
||||
$output = instant_remote_process([$listCommand], $server, false);
|
||||
|
||||
if (empty($output)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$images = collect(explode("\n", trim($output)))
|
||||
->filter()
|
||||
->map(function ($line) {
|
||||
$parts = explode('#', $line);
|
||||
$imageRef = $parts[0] ?? '';
|
||||
$tagParts = explode(':', $imageRef);
|
||||
|
||||
return [
|
||||
'repository' => $tagParts[0] ?? '',
|
||||
'tag' => $tagParts[1] ?? '',
|
||||
'created_at' => $parts[1] ?? '',
|
||||
'image_ref' => $imageRef,
|
||||
];
|
||||
})
|
||||
->filter(fn ($image) => ! empty($image['tag']));
|
||||
|
||||
// Separate images into categories
|
||||
// PR images (pr-*) and build images (*-build) are excluded from retention
|
||||
// Build images will be cleaned up by docker image prune -af
|
||||
$prImages = $images->filter(fn ($image) => str_starts_with($image['tag'], 'pr-'));
|
||||
$regularImages = $images->filter(fn ($image) => ! str_starts_with($image['tag'], 'pr-') && ! str_ends_with($image['tag'], '-build'));
|
||||
|
||||
// Always delete all PR images
|
||||
foreach ($prImages as $image) {
|
||||
$deleteCommand = "docker rmi {$image['image_ref']} 2>/dev/null || true";
|
||||
$deleteOutput = instant_remote_process([$deleteCommand], $server, false);
|
||||
$cleanupLog[] = [
|
||||
'command' => $deleteCommand,
|
||||
'output' => $deleteOutput ?? 'PR image removed or was in use',
|
||||
];
|
||||
}
|
||||
|
||||
// Filter out current running image from regular images and sort by creation date
|
||||
$sortedRegularImages = $regularImages
|
||||
->filter(fn ($image) => $image['tag'] !== $currentTag)
|
||||
->sortByDesc('created_at')
|
||||
->values();
|
||||
|
||||
// Keep only N images (imagesToKeep), delete the rest
|
||||
$imagesToDelete = $sortedRegularImages->skip($imagesToKeep);
|
||||
|
||||
foreach ($imagesToDelete as $image) {
|
||||
$deleteCommand = "docker rmi {$image['image_ref']} 2>/dev/null || true";
|
||||
$deleteOutput = instant_remote_process([$deleteCommand], $server, false);
|
||||
$cleanupLog[] = [
|
||||
'command' => $deleteCommand,
|
||||
'output' => $deleteOutput ?? 'Image removed or was in use',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanupLog;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class StopService
|
||||
{
|
||||
|
|
@ -17,6 +19,17 @@ class StopService
|
|||
public function handle(Service $service, bool $deleteConnectedNetworks = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
// Cancel any in-progress deployment activities so status doesn't stay stuck at "starting"
|
||||
Activity::where('properties->type_uuid', $service->uuid)
|
||||
->where(function ($q) {
|
||||
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
|
||||
->orWhere('properties->status', ProcessStatus::QUEUED->value);
|
||||
})
|
||||
->each(function ($activity) {
|
||||
$activity->properties = $activity->properties->put('status', ProcessStatus::CANCELLED->value);
|
||||
$activity->save();
|
||||
});
|
||||
|
||||
$server = $service->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CheckTraefikVersionJob;
|
||||
|
|
@ -100,17 +99,7 @@ private function pullImages(): void
|
|||
} else {
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error pulling images: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
// Sentinel update checks are now handled by ServerManagerJob
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)
|
||||
->cron($this->updateCheckFrequency)
|
||||
->timezone($this->instanceTimezone)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ class ProxyStatusChangedUI implements ShouldBroadcast
|
|||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct(?int $teamId = null)
|
||||
public ?int $activityId = null;
|
||||
|
||||
public function __construct(?int $teamId = null, ?int $activityId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
$this->activityId = $activityId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ public static function generateScpCommand(Server $server, string $source, string
|
|||
return $scp_command;
|
||||
}
|
||||
|
||||
public static function generateSshCommand(Server $server, string $command)
|
||||
public static function generateSshCommand(Server $server, string $command, bool $disableMultiplexing = false)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
|
|
@ -168,7 +168,7 @@ public static function generateSshCommand(Server $server, string $command)
|
|||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
$multiplexingSuccessful = false;
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (! $disableMultiplexing && self::isMultiplexingEnabled()) {
|
||||
try {
|
||||
$multiplexingSuccessful = self::ensureMultiplexedConnection($server);
|
||||
if ($multiplexingSuccessful) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Livewire\Project\Service\Storage;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
|
|
@ -15,23 +14,6 @@ class Bitbucket extends Controller
|
|||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Bitbicket::manual_bitbucket", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$headers = $request->headers->all();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -18,30 +17,6 @@ public function manual(Request $request)
|
|||
try {
|
||||
$return_payloads = collect([]);
|
||||
$x_gitea_delivery = request()->header('X-Gitea-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) {
|
||||
return Str::contains($file, $x_gitea_delivery);
|
||||
})->first();
|
||||
if ($gitea_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitea::manual_{$x_gitea_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_gitea_event = Str::lower($request->header('X-Gitea-Event'));
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
$content_type = $request->header('Content-Type');
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -25,30 +24,6 @@ public function manual(Request $request)
|
|||
try {
|
||||
$return_payloads = collect([]);
|
||||
$x_github_delivery = request()->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::manual_{$x_github_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
$content_type = $request->header('Content-Type');
|
||||
|
|
@ -310,30 +285,6 @@ public function normal(Request $request)
|
|||
$return_payloads = collect([]);
|
||||
$id = null;
|
||||
$x_github_delivery = $request->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::normal_{$x_github_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
|
||||
$x_github_hook_installation_target_id = $request->header('X-GitHub-Hook-Installation-Target-Id');
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
|
|
@ -624,23 +575,6 @@ public function install(Request $request)
|
|||
{
|
||||
try {
|
||||
$installation_id = $request->get('installation_id');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::install_{$installation_id}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$source = $request->get('source');
|
||||
$setup_action = $request->get('setup_action');
|
||||
$github_app = GithubApp::where('uuid', $source)->firstOrFail();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -16,24 +15,6 @@ class Gitlab extends Controller
|
|||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitlab::manual_gitlab", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$headers = $request->headers->all();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Jobs\StripeProcessJob;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Stripe extends Controller
|
||||
{
|
||||
|
|
@ -20,23 +19,6 @@ public function events(Request $request)
|
|||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
|
||||
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
}
|
||||
StripeProcessJob::dispatch($event);
|
||||
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@ private function deploy_docker_compose_buildpack()
|
|||
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
||||
}
|
||||
} else {
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'), commit: $this->commit);
|
||||
// Always add .env file to services
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
|
|
@ -670,13 +670,20 @@ private function deploy_docker_compose_buildpack()
|
|||
$build_command = "DOCKER_BUILDKIT=1 {$build_command}";
|
||||
}
|
||||
|
||||
// Append build arguments if not using build secrets (matching default behavior)
|
||||
// Inject build arguments after build subcommand if not using build secrets
|
||||
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
|
||||
$build_args_string = $this->build_args->implode(' ');
|
||||
// Escape single quotes for bash -c context used by executeInDocker
|
||||
$build_args_string = str_replace("'", "'\\''", $build_args_string);
|
||||
$build_command .= " {$build_args_string}";
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.');
|
||||
|
||||
// Inject build args right after 'build' subcommand (not at the end)
|
||||
$original_command = $build_command;
|
||||
$build_command = injectDockerComposeBuildArgs($build_command, $build_args_string);
|
||||
|
||||
// Only log if build args were actually injected (command was modified)
|
||||
if ($build_command !== $original_command) {
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->execute_remote_command(
|
||||
|
|
@ -1806,9 +1813,9 @@ private function health_check()
|
|||
$this->application->update(['status' => 'running']);
|
||||
$this->application_deployment_queue->addLogEntry('New container is healthy.');
|
||||
break;
|
||||
}
|
||||
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
} elseif (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
$this->newVersionIsHealthy = false;
|
||||
$this->application_deployment_queue->addLogEntry('New container is unhealthy.', type: 'error');
|
||||
$this->query_logs();
|
||||
break;
|
||||
}
|
||||
|
|
@ -2274,13 +2281,13 @@ private function generate_nixpacks_env_variables()
|
|||
$this->env_nixpacks_args = collect([]);
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->nixpacks_environment_variables as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
|
|
@ -2289,7 +2296,10 @@ private function generate_nixpacks_env_variables()
|
|||
// Add COOLIFY_* environment variables to Nixpacks build context
|
||||
$coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
|
||||
$coolify_envs->each(function ($value, $key) {
|
||||
$this->env_nixpacks_args->push("--env {$key}={$value}");
|
||||
// Only add environment variables with non-null and non-empty values
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$key}={$value}");
|
||||
}
|
||||
});
|
||||
|
||||
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
||||
|
|
@ -3180,6 +3190,18 @@ private function stop_running_container(bool $force = false)
|
|||
$this->graceful_shutdown_container($this->container_name);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// If new version is healthy, this is just cleanup - don't fail the deployment
|
||||
if ($this->newVersionIsHealthy || $force) {
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
"Warning: Could not remove old container: {$e->getMessage()}",
|
||||
'stderr',
|
||||
hidden: true
|
||||
);
|
||||
|
||||
return; // Don't re-throw - cleanup failures shouldn't fail successful deployments
|
||||
}
|
||||
|
||||
// Only re-throw if deployment hasn't succeeded yet
|
||||
throw new DeploymentException("Failed to stop running container: {$e->getMessage()}", $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\TraefikVersionOutdated;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
|
@ -38,6 +39,8 @@ public function handle(): void
|
|||
$this->server->update(['detected_traefik_version' => $currentVersion]);
|
||||
|
||||
if (! $currentVersion) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -48,16 +51,22 @@ public function handle(): void
|
|||
|
||||
// Handle empty/null response from SSH command
|
||||
if (empty(trim($imageTag))) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains(strtolower(trim($imageTag)), ':latest')) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse current version to extract major.minor.patch
|
||||
$current = ltrim($currentVersion, 'v');
|
||||
if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +86,8 @@ public function handle(): void
|
|||
$this->server->update(['traefik_outdated_info' => null]);
|
||||
}
|
||||
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +107,9 @@ public function handle(): void
|
|||
// Fully up to date
|
||||
$this->server->update(['traefik_outdated_info' => null]);
|
||||
}
|
||||
|
||||
// Dispatch UI update event so warning state refreshes in real-time
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep POSTGRES_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$user = $envs->filter(function ($env) {
|
||||
|
|
@ -152,7 +152,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep MYSQL_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
|
|
@ -175,7 +175,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
|
||||
|
|
@ -217,7 +217,7 @@ public function handle(): void
|
|||
try {
|
||||
$commands = [];
|
||||
$commands[] = "docker exec $this->container_name env | grep MONGO_INITDB_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
|
||||
if (filled($envs)) {
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
|
@ -508,7 +508,7 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
|
|||
}
|
||||
}
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -537,7 +537,7 @@ private function backup_standalone_postgresql(string $database): void
|
|||
}
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -560,7 +560,7 @@ private function backup_standalone_mysql(string $database): void
|
|||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -583,7 +583,7 @@ private function backup_standalone_mariadb(string $database): void
|
|||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -614,7 +614,7 @@ private function add_to_error_output($output): void
|
|||
|
||||
private function calculate_size()
|
||||
{
|
||||
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
|
||||
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
private function upload_to_s3(): void
|
||||
|
|
@ -637,9 +637,9 @@ private function upload_to_s3(): void
|
|||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup_log_uuid}"], $this->server, false);
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup_log_uuid}"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
if (filled($containerExists)) {
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup_log_uuid}"], $this->server, false);
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup_log_uuid}"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
if (isDev()) {
|
||||
|
|
@ -661,7 +661,7 @@ private function upload_to_s3(): void
|
|||
|
||||
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
|
||||
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||
instant_remote_process($commands, $this->server);
|
||||
instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
|
||||
$this->s3_uploaded = true;
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -670,7 +670,7 @@ private function upload_to_s3(): void
|
|||
throw $e;
|
||||
} finally {
|
||||
$command = "docker rm -f backup-of-{$this->backup_log_uuid}";
|
||||
instant_remote_process([$command], $this->server);
|
||||
instant_remote_process([$command], $this->server, true, false, null, disableMultiplexing: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,8 +300,9 @@ private function aggregateMultiContainerStatuses()
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update application status with aggregated result
|
||||
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
||||
|
|
@ -360,8 +361,9 @@ private function aggregateServiceContainerStatuses()
|
|||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -19,11 +22,13 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
public $timeout = 120;
|
||||
|
||||
public ?int $activity_id = null;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(120)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
|
@ -31,15 +36,125 @@ public function __construct(public Server $server) {}
|
|||
public function handle()
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server, restarting: true);
|
||||
|
||||
// Set status to restarting
|
||||
$this->server->proxy->status = 'restarting';
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
StartProxy::run($this->server, force: true, restarting: true);
|
||||
// Build combined stop + start commands for a single activity
|
||||
$commands = $this->buildRestartCommands();
|
||||
|
||||
// Create activity and dispatch immediately - returns Activity right away
|
||||
// The remote_process runs asynchronously, so UI gets activity ID instantly
|
||||
$activity = remote_process(
|
||||
$commands,
|
||||
$this->server,
|
||||
callEventOnFinish: 'ProxyStatusChanged',
|
||||
callEventData: $this->server->id
|
||||
);
|
||||
|
||||
// Store activity ID and notify UI immediately with it
|
||||
$this->activity_id = $activity->id;
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id, $this->activity_id);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Set error status
|
||||
$this->server->proxy->status = 'error';
|
||||
$this->server->save();
|
||||
|
||||
// Notify UI of error
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
// Clear dashboard cache on error
|
||||
ProxyDashboardCacheService::clearCache($this->server);
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build combined stop + start commands for proxy restart.
|
||||
* This creates a single command sequence that shows all logs in one activity.
|
||||
*/
|
||||
private function buildRestartCommands(): array
|
||||
{
|
||||
$proxyType = $this->server->proxyType();
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$stopTimeout = 30;
|
||||
|
||||
// Get proxy configuration
|
||||
$configuration = GetProxyConfiguration::run($this->server);
|
||||
if (! $configuration) {
|
||||
throw new \Exception('Configuration is not synced');
|
||||
}
|
||||
SaveProxyConfiguration::run($this->server, $configuration);
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$this->server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
|
||||
$this->server->save();
|
||||
|
||||
$commands = collect([]);
|
||||
|
||||
// === STOP PHASE ===
|
||||
$commands = $commands->merge([
|
||||
"echo 'Stopping proxy...'",
|
||||
"docker stop -t=$stopTimeout $containerName 2>/dev/null || true",
|
||||
"docker rm -f $containerName 2>/dev/null || true",
|
||||
'# Wait for container to be fully removed',
|
||||
'for i in {1..15}; do',
|
||||
" if ! docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
|
||||
" echo 'Container removed successfully.'",
|
||||
' break',
|
||||
' fi',
|
||||
' echo "Waiting for container to be removed... ($i/15)"',
|
||||
' sleep 1',
|
||||
' # Force remove on each iteration in case it got stuck',
|
||||
" docker rm -f $containerName 2>/dev/null || true",
|
||||
'done',
|
||||
'# Final verification and force cleanup',
|
||||
"if docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
|
||||
" echo 'Container still exists after wait, forcing removal...'",
|
||||
" docker rm -f $containerName 2>/dev/null || true",
|
||||
' sleep 2',
|
||||
'fi',
|
||||
"echo 'Proxy stopped successfully.'",
|
||||
]);
|
||||
|
||||
// === START PHASE ===
|
||||
if ($this->server->isSwarmManager()) {
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting proxy (Swarm mode)...'",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker stack deploy --detach=true -c docker-compose.yml coolify-proxy',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
if (isDev() && $proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = '/data/coolify/proxy/caddy';
|
||||
}
|
||||
$caddyfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting proxy...'",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
]);
|
||||
// Ensure required networks exist BEFORE docker compose up
|
||||
$commands = $commands->merge(ensureProxyNetworksExist($this->server));
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --wait --remove-orphans',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($this->server));
|
||||
}
|
||||
|
||||
return $commands->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,9 @@ public function handle(): void
|
|||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
|
||||
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
|
||||
$exec = "docker exec {$containerName} {$cmd}";
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
||||
// Disable SSH multiplexing to prevent race conditions when multiple tasks run concurrently
|
||||
// See: https://github.com/coollabsio/coolify/issues/6736
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->task_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $this->task_output,
|
||||
|
|
|
|||
|
|
@ -111,34 +111,48 @@ private function processScheduledTasks(Collection $servers): void
|
|||
|
||||
private function processServerTasks(Server $server): void
|
||||
{
|
||||
// Get server timezone (used for all scheduled tasks)
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Check if we should run sentinel-based checks
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
$waitTime = $server->waitBeforeDoingSshCheck();
|
||||
$sentinelOutOfSync = Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($waitTime));
|
||||
$sentinelOutOfSync = Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->copy()->subSeconds($waitTime));
|
||||
|
||||
if ($sentinelOutOfSync) {
|
||||
// Dispatch jobs if Sentinel is out of sync
|
||||
if ($this->shouldRunNow($this->checkFrequency)) {
|
||||
// Dispatch ServerCheckJob if Sentinel is out of sync
|
||||
if ($this->shouldRunNow($this->checkFrequency, $serverTimezone)) {
|
||||
ServerCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch ServerStorageCheckJob if due
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
$isSentinelEnabled = $server->isSentinelEnabled();
|
||||
$shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone);
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
|
||||
if ($shouldRestartSentinel) {
|
||||
dispatch(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch ServerStorageCheckJob if due (only when Sentinel is out of sync or disabled)
|
||||
// When Sentinel is active, PushServerUpdateJob handles storage checks with real-time data
|
||||
if ($sentinelOutOfSync) {
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 23 * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$shouldRunStorageCheck = $this->shouldRunNow($serverDiskUsageCheckFrequency);
|
||||
$shouldRunStorageCheck = $this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone);
|
||||
|
||||
if ($shouldRunStorageCheck) {
|
||||
ServerStorageCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Dispatch ServerPatchCheckJob if due (weekly)
|
||||
$shouldRunPatchCheck = $this->shouldRunNow('0 0 * * 0', $serverTimezone);
|
||||
|
||||
|
|
@ -146,14 +160,13 @@ private function processServerTasks(Server $server): void
|
|||
ServerPatchCheckJob::dispatch($server);
|
||||
}
|
||||
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
$isSentinelEnabled = $server->isSentinelEnabled();
|
||||
$shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone);
|
||||
// Check for sentinel updates hourly (independent of user-configurable update_check_frequency)
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$shouldCheckSentinel = $this->shouldRunNow('0 * * * *', $serverTimezone);
|
||||
|
||||
if ($shouldRestartSentinel) {
|
||||
dispatch(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
});
|
||||
if ($shouldCheckSentinel) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled as EventsMaintenanceModeDisabled;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||
|
||||
class MaintenanceModeDisabledNotification
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(EventsMaintenanceModeDisabled $event): void
|
||||
{
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$files = collect($files);
|
||||
$files = $files->sort();
|
||||
foreach ($files as $file) {
|
||||
$content = Storage::disk('webhooks-during-maintenance')->get($file);
|
||||
$data = json_decode($content, true);
|
||||
$symfonyRequest = new SymfonyRequest(
|
||||
$data['query'],
|
||||
$data['request'],
|
||||
$data['attributes'],
|
||||
$data['cookies'],
|
||||
$data['files'],
|
||||
$data['server'],
|
||||
$data['content']
|
||||
);
|
||||
|
||||
foreach ($data['headers'] as $key => $value) {
|
||||
$symfonyRequest->headers->set($key, $value);
|
||||
}
|
||||
$request = Request::createFromBase($symfonyRequest);
|
||||
$endpoint = str($file)->after('_')->beforeLast('_')->value();
|
||||
$class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value());
|
||||
$method = str($endpoint)->after('::')->value();
|
||||
try {
|
||||
$instance = new $class;
|
||||
$instance->$method($request);
|
||||
} catch (\Throwable $th) {
|
||||
} finally {
|
||||
Storage::disk('webhooks-during-maintenance')->delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled as EventsMaintenanceModeEnabled;
|
||||
|
||||
class MaintenanceModeEnabledNotification
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(EventsMaintenanceModeEnabled $event): void {}
|
||||
}
|
||||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Jobs\CheckTraefikVersionForServerJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
|
||||
{
|
||||
|
|
@ -32,6 +35,19 @@ public function handle(ProxyStatusChanged $event)
|
|||
$server->setupDynamicProxyConfiguration();
|
||||
$server->proxy->force_stop = false;
|
||||
$server->save();
|
||||
|
||||
// Check Traefik version after proxy is running
|
||||
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$traefikVersions = get_traefik_versions();
|
||||
if ($traefikVersions !== null) {
|
||||
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
|
||||
} else {
|
||||
Log::warning('Traefik version check skipped after proxy status change: versions.json data unavailable', [
|
||||
'server_id' => $server->id,
|
||||
'server_name' => $server->name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($status === 'created') {
|
||||
instant_remote_process([
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Show extends Component
|
|||
|
||||
public $isKeepAliveOn = true;
|
||||
|
||||
public bool $is_debug_enabled = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
|
@ -56,9 +58,23 @@ public function mount()
|
|||
$this->application_deployment_queue = $application_deployment_queue;
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->deployment_uuid = $deploymentUuid;
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->isKeepAliveOn();
|
||||
}
|
||||
|
||||
public function toggleDebug()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
|
||||
$this->application->settings->save();
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->application_deployment_queue->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshQueue()
|
||||
{
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false, $showToast = true)
|
||||
public function loadComposeFile($isInit = false, $showToast = true, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
|
@ -530,7 +530,7 @@ public function loadComposeFile($isInit = false, $showToast = true)
|
|||
return;
|
||||
}
|
||||
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit, $restoreBaseDirectory, $restoreDockerComposeLocation);
|
||||
if (is_null($this->parsedServices)) {
|
||||
$showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
|
|
@ -606,13 +606,6 @@ public function generateDomain(string $serviceName)
|
|||
}
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsStatic($value)
|
||||
{
|
||||
if ($value) {
|
||||
|
|
@ -786,11 +779,13 @@ public function submit($showToaster = true)
|
|||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
|
||||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
|
||||
$oldBaseDirectory = $this->application->base_directory;
|
||||
|
||||
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||
|
|
@ -821,6 +816,42 @@ public function submit($showToaster = true)
|
|||
return; // Stop if there are conflicts and user hasn't confirmed
|
||||
}
|
||||
|
||||
// Normalize paths BEFORE validation
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
|
||||
// Validate docker compose file path BEFORE saving to database
|
||||
// This prevents invalid paths from being persisted when validation fails
|
||||
if ($this->buildPack === 'dockercompose' &&
|
||||
($oldDockerComposeLocation !== $this->dockerComposeLocation ||
|
||||
$oldBaseDirectory !== $this->baseDirectory)) {
|
||||
// Pass original values to loadComposeFile so it can restore them on failure
|
||||
// The finally block in Application::loadComposeFile will save these original
|
||||
// values if validation fails, preventing invalid paths from being persisted
|
||||
$compose_return = $this->loadComposeFile(
|
||||
isInit: false,
|
||||
showToast: false,
|
||||
restoreBaseDirectory: $oldBaseDirectory,
|
||||
restoreDockerComposeLocation: $oldDockerComposeLocation
|
||||
);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
// Validation failed - restore original values to component properties
|
||||
$this->baseDirectory = $oldBaseDirectory;
|
||||
$this->dockerComposeLocation = $oldDockerComposeLocation;
|
||||
// The model was saved by loadComposeFile's finally block with original values
|
||||
// Refresh to sync component with database state
|
||||
$this->application->refresh();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->application->save();
|
||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
|
|
@ -828,13 +859,6 @@ public function submit($showToaster = true)
|
|||
$this->application->save();
|
||||
}
|
||||
|
||||
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
|
|
@ -855,14 +879,6 @@ public function submit($showToaster = true)
|
|||
$this->application->ports_exposes = $port;
|
||||
}
|
||||
}
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
|
|
@ -1018,11 +1034,27 @@ public function getDockerComposeBuildCommandPreviewProperty(): string
|
|||
// Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml)
|
||||
// Actual deployment uses absolute path: /artifacts/{deployment_uuid}{base_directory}{docker_compose_location}
|
||||
// Build-time env path references ApplicationDeploymentJob::BUILD_TIME_ENV_PATH as source of truth
|
||||
return injectDockerComposeFlags(
|
||||
$command = injectDockerComposeFlags(
|
||||
$this->dockerComposeCustomBuildCommand,
|
||||
".{$normalizedBase}{$this->dockerComposeLocation}",
|
||||
\App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
|
||||
);
|
||||
|
||||
// Inject build args if not using build secrets
|
||||
if (! $this->application->settings->use_build_secrets) {
|
||||
$buildTimeEnvs = $this->application->environment_variables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
if ($buildTimeEnvs->isNotEmpty()) {
|
||||
$buildArgs = generateDockerBuildArgs($buildTimeEnvs);
|
||||
$buildArgsString = $buildArgs->implode(' ');
|
||||
|
||||
$command = injectDockerComposeBuildArgs($command, $buildArgsString);
|
||||
}
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
public function getDockerComposeStartCommandPreviewProperty(): string
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -19,9 +20,30 @@ class Rollback extends Component
|
|||
|
||||
public array $parameters;
|
||||
|
||||
#[Validate(['integer', 'min:0', 'max:100'])]
|
||||
public int $dockerImagesToKeep = 2;
|
||||
|
||||
public bool $serverRetentionDisabled = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->dockerImagesToKeep = $this->application->settings->docker_images_to_keep ?? 2;
|
||||
$server = $this->application->destination->server;
|
||||
$this->serverRetentionDisabled = $server->settings->disable_application_image_retention ?? false;
|
||||
}
|
||||
|
||||
public function saveSettings()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->validate();
|
||||
$this->application->settings->docker_images_to_keep = $this->dockerImagesToKeep;
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function rollbackImage($commit)
|
||||
|
|
@ -66,14 +88,12 @@ public function loadImages($showToast = false)
|
|||
return str($item)->contains($image);
|
||||
})->map(function ($item) {
|
||||
$item = str($item)->explode('#');
|
||||
if ($item[1] === $this->current) {
|
||||
// $is_current = true;
|
||||
}
|
||||
$is_current = $item[1] === $this->current;
|
||||
|
||||
return [
|
||||
'tag' => $item[1],
|
||||
'created_at' => $item[2],
|
||||
'is_current' => $is_current ?? null,
|
||||
'is_current' => $is_current,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,16 +75,6 @@ public function mount()
|
|||
$this->github_apps = GithubApp::private();
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->base_directory) {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
if (! str($this->base_directory)->startsWith('/')) {
|
||||
$this->base_directory = '/'.$this->base_directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
|
|
|||
|
|
@ -107,26 +107,6 @@ public function mount()
|
|||
$this->query = request()->query();
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->base_directory) {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
if (! str($this->base_directory)->startsWith('/')) {
|
||||
$this->base_directory = '/'.$this->base_directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDockerComposeLocation()
|
||||
{
|
||||
if ($this->docker_compose_location) {
|
||||
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
|
||||
if (! str($this->docker_compose_location)->startsWith('/')) {
|
||||
$this->docker_compose_location = '/'.$this->docker_compose_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,21 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function instantSaveSettings()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
// Save checkbox states without port validation
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -39,10 +39,14 @@ class GetLogs extends Component
|
|||
|
||||
public ?bool $streamLogs = false;
|
||||
|
||||
public ?bool $showTimeStamps = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
|
||||
public ?int $numberOfLines = 100;
|
||||
|
||||
public bool $expandByDefault = false;
|
||||
|
||||
public bool $collapsible = true;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
|
|
@ -92,12 +96,33 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function toggleTimestamps()
|
||||
{
|
||||
$previousValue = $this->showTimeStamps;
|
||||
$this->showTimeStamps = ! $this->showTimeStamps;
|
||||
|
||||
try {
|
||||
$this->instantSave();
|
||||
$this->getLogs(true);
|
||||
} catch (\Throwable $e) {
|
||||
// Revert the flag to its previous value on failure
|
||||
$this->showTimeStamps = $previousValue;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleStreamLogs()
|
||||
{
|
||||
$this->streamLogs = ! $this->streamLogs;
|
||||
}
|
||||
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Add extends Component
|
|||
'command' => 'required|string',
|
||||
'frequency' => 'required|string',
|
||||
'container' => 'nullable|string',
|
||||
'timeout' => 'required|integer|min:60|max:3600',
|
||||
'timeout' => 'required|integer|min:60|max:36000',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class Show extends Component
|
|||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $container = null;
|
||||
|
||||
#[Validate(['integer', 'required', 'min:60', 'max:3600'])]
|
||||
#[Validate(['integer', 'required', 'min:60', 'max:36000'])]
|
||||
public $timeout = 300;
|
||||
|
||||
#[Locked]
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ class DockerCleanup extends Component
|
|||
#[Validate('boolean')]
|
||||
public bool $deleteUnusedNetworks = false;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $disableApplicationImageRetention = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
|
|
@ -52,6 +55,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
|
||||
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
|
||||
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
|
||||
$this->server->settings->disable_application_image_retention = $this->disableApplicationImageRetention;
|
||||
$this->server->settings->save();
|
||||
} else {
|
||||
$this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
|
||||
|
|
@ -59,6 +63,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
|
||||
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
|
||||
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
|
||||
$this->disableApplicationImageRetention = $this->server->settings->disable_application_image_retention;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@
|
|||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Jobs\CheckTraefikVersionForServerJob;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
|
||||
class Navbar extends Component
|
||||
|
|
@ -29,6 +28,10 @@ class Navbar extends Component
|
|||
|
||||
public ?string $proxyStatus = 'unknown';
|
||||
|
||||
public ?string $lastNotifiedStatus = null;
|
||||
|
||||
public bool $restartInitiated = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
|
@ -63,27 +66,19 @@ public function restart()
|
|||
{
|
||||
try {
|
||||
$this->authorize('manageProxy', $this->server);
|
||||
StopProxy::run($this->server, restarting: true);
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
$activity = StartProxy::run($this->server, force: true, restarting: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
|
||||
// Check Traefik version after restart to provide immediate feedback
|
||||
if ($this->server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$traefikVersions = get_traefik_versions();
|
||||
if ($traefikVersions !== null) {
|
||||
CheckTraefikVersionForServerJob::dispatch($this->server, $traefikVersions);
|
||||
} else {
|
||||
Log::warning('Traefik version check skipped: versions.json data unavailable', [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
}
|
||||
// Prevent duplicate restart calls
|
||||
if ($this->restartInitiated) {
|
||||
return;
|
||||
}
|
||||
$this->restartInitiated = true;
|
||||
|
||||
// Always use background job for all servers
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->restartInitiated = false;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
|
@ -137,12 +132,27 @@ public function checkProxyStatus()
|
|||
}
|
||||
}
|
||||
|
||||
public function showNotification()
|
||||
public function showNotification($event = null)
|
||||
{
|
||||
$previousStatus = $this->proxyStatus;
|
||||
$this->server->refresh();
|
||||
$this->proxyStatus = $this->server->proxy->status ?? 'unknown';
|
||||
|
||||
// If event contains activityId, open activity monitor
|
||||
if ($event && isset($event['activityId'])) {
|
||||
$this->dispatch('activityMonitor', $event['activityId']);
|
||||
}
|
||||
|
||||
// Reset restart flag when proxy reaches a stable state
|
||||
if (in_array($this->proxyStatus, ['running', 'exited', 'error'])) {
|
||||
$this->restartInitiated = false;
|
||||
}
|
||||
|
||||
// Skip notification if we already notified about this status (prevents duplicates)
|
||||
if ($this->lastNotifiedStatus === $this->proxyStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->proxyStatus) {
|
||||
case 'running':
|
||||
$this->loadProxyConfiguration();
|
||||
|
|
@ -150,6 +160,7 @@ public function showNotification()
|
|||
// Don't show during normal start/restart flows (starting, restarting, stopping)
|
||||
if (in_array($previousStatus, ['exited', 'stopped', 'unknown', null])) {
|
||||
$this->dispatch('success', 'Proxy is running.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
}
|
||||
break;
|
||||
case 'exited':
|
||||
|
|
@ -157,19 +168,30 @@ public function showNotification()
|
|||
// Don't show during normal stop/restart flows (stopping, restarting)
|
||||
if (in_array($previousStatus, ['running'])) {
|
||||
$this->dispatch('info', 'Proxy has exited.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
}
|
||||
break;
|
||||
case 'stopping':
|
||||
$this->dispatch('info', 'Proxy is stopping.');
|
||||
// $this->dispatch('info', 'Proxy is stopping.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'starting':
|
||||
$this->dispatch('info', 'Proxy is starting.');
|
||||
// $this->dispatch('info', 'Proxy is starting.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'restarting':
|
||||
// $this->dispatch('info', 'Proxy is restarting.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'error':
|
||||
$this->dispatch('error', 'Proxy restart failed. Check logs.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'unknown':
|
||||
$this->dispatch('info', 'Proxy status is unknown.');
|
||||
// Don't notify for unknown status - too noisy
|
||||
break;
|
||||
default:
|
||||
$this->dispatch('info', 'Proxy status updated.');
|
||||
// Don't notify for other statuses
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1500,10 +1500,10 @@ public function oldRawParser()
|
|||
instant_remote_process($commands, $this->destination->server, false);
|
||||
}
|
||||
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null)
|
||||
{
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return applicationParser($this, $pull_request_id, $preview_id);
|
||||
return applicationParser($this, $pull_request_id, $preview_id, $commit);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||
} else {
|
||||
|
|
@ -1511,9 +1511,11 @@ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
|||
}
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
|
||||
{
|
||||
$initialDockerComposeLocation = $this->docker_compose_location;
|
||||
// Use provided restore values or capture current values as fallback
|
||||
$initialDockerComposeLocation = $restoreDockerComposeLocation ?? $this->docker_compose_location;
|
||||
$initialBaseDirectory = $restoreBaseDirectory ?? $this->base_directory;
|
||||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1580,6 +1582,7 @@ public function loadComposeFile($isInit = false)
|
|||
throw new \RuntimeException($e->getMessage());
|
||||
} finally {
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class ApplicationSetting extends Model
|
|||
'is_git_submodules_enabled' => 'boolean',
|
||||
'is_git_lfs_enabled' => 'boolean',
|
||||
'is_git_shallow_clone_enabled' => 'boolean',
|
||||
'docker_images_to_keep' => 'integer',
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ protected static function booted()
|
|||
'value' => $environment_variable->value,
|
||||
'is_multiline' => $environment_variable->is_multiline ?? false,
|
||||
'is_literal' => $environment_variable->is_literal ?? false,
|
||||
'is_runtime' => $environment_variable->is_runtime ?? false,
|
||||
'is_buildtime' => $environment_variable->is_buildtime ?? false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $environment_variable->resourceable_id,
|
||||
'is_preview' => true,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class ServerSetting extends Model
|
|||
'is_reachable' => 'boolean',
|
||||
'is_usable' => 'boolean',
|
||||
'is_terminal_enabled' => 'boolean',
|
||||
'disable_application_image_retention' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -712,6 +712,84 @@ public function extraFields()
|
|||
|
||||
$fields->put('MinIO', $data->toArray());
|
||||
break;
|
||||
case $image->contains('garage'):
|
||||
$data = collect([]);
|
||||
$s3_api_url = $this->environment_variables()->where('key', 'GARAGE_S3_API_URL')->first();
|
||||
$web_url = $this->environment_variables()->where('key', 'GARAGE_WEB_URL')->first();
|
||||
$admin_url = $this->environment_variables()->where('key', 'GARAGE_ADMIN_URL')->first();
|
||||
$admin_token = $this->environment_variables()->where('key', 'GARAGE_ADMIN_TOKEN')->first();
|
||||
if (is_null($admin_token)) {
|
||||
$admin_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGE')->first();
|
||||
}
|
||||
$rpc_secret = $this->environment_variables()->where('key', 'GARAGE_RPC_SECRET')->first();
|
||||
if (is_null($rpc_secret)) {
|
||||
$rpc_secret = $this->environment_variables()->where('key', 'SERVICE_HEX_32_RPCSECRET')->first();
|
||||
}
|
||||
$metrics_token = $this->environment_variables()->where('key', 'GARAGE_METRICS_TOKEN')->first();
|
||||
if (is_null($metrics_token)) {
|
||||
$metrics_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGEMETRICS')->first();
|
||||
}
|
||||
|
||||
if ($s3_api_url) {
|
||||
$data = $data->merge([
|
||||
'S3 API URL' => [
|
||||
'key' => data_get($s3_api_url, 'key'),
|
||||
'value' => data_get($s3_api_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($web_url) {
|
||||
$data = $data->merge([
|
||||
'Web URL' => [
|
||||
'key' => data_get($web_url, 'key'),
|
||||
'value' => data_get($web_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_url) {
|
||||
$data = $data->merge([
|
||||
'Admin URL' => [
|
||||
'key' => data_get($admin_url, 'key'),
|
||||
'value' => data_get($admin_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_token) {
|
||||
$data = $data->merge([
|
||||
'Admin Token' => [
|
||||
'key' => data_get($admin_token, 'key'),
|
||||
'value' => data_get($admin_token, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($rpc_secret) {
|
||||
$data = $data->merge([
|
||||
'RPC Secret' => [
|
||||
'key' => data_get($rpc_secret, 'key'),
|
||||
'value' => data_get($rpc_secret, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($metrics_token) {
|
||||
$data = $data->merge([
|
||||
'Metrics Token' => [
|
||||
'key' => data_get($metrics_token, 'key'),
|
||||
'value' => data_get($metrics_token, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$fields->put('Garage', $data->toArray());
|
||||
break;
|
||||
case $image->contains('weblate'):
|
||||
$data = collect([]);
|
||||
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
|
||||
|
|
|
|||
|
|
@ -43,9 +43,19 @@ public function toMail($notifiable = null): MailMessage
|
|||
$mail = new MailMessage;
|
||||
$count = $this->servers->count();
|
||||
|
||||
// Transform servers to include URLs
|
||||
$serversWithUrls = $this->servers->map(function ($server) {
|
||||
return [
|
||||
'name' => $server->name,
|
||||
'uuid' => $server->uuid,
|
||||
'url' => base_url().'/server/'.$server->uuid.'/proxy',
|
||||
'outdatedInfo' => $server->outdatedInfo ?? [],
|
||||
];
|
||||
});
|
||||
|
||||
$mail->subject("Coolify: Traefik proxy outdated on {$count} server(s)");
|
||||
$mail->view('emails.traefik-version-outdated', [
|
||||
'servers' => $this->servers,
|
||||
'servers' => $serversWithUrls,
|
||||
'count' => $count,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Listeners\MaintenanceModeDisabledNotification;
|
||||
use App\Listeners\MaintenanceModeEnabledNotification;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
|
||||
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||
|
|
@ -19,12 +15,6 @@
|
|||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $listen = [
|
||||
MaintenanceModeEnabled::class => [
|
||||
MaintenanceModeEnabledNotification::class,
|
||||
],
|
||||
MaintenanceModeDisabled::class => [
|
||||
MaintenanceModeDisabledNotification::class,
|
||||
],
|
||||
SocialiteWasCalled::class => [
|
||||
AzureExtendSocialite::class.'@handle',
|
||||
AuthentikExtendSocialite::class.'@handle',
|
||||
|
|
|
|||
|
|
@ -16,14 +16,23 @@
|
|||
* UI components transform this to human-readable format (e.g., "Running (Healthy)").
|
||||
*
|
||||
* State Priority (highest to lowest):
|
||||
* 1. Restarting → degraded:unhealthy
|
||||
* 2. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 3. Mixed (running + exited) → degraded:unhealthy
|
||||
* 4. Running → running:healthy/unhealthy/unknown
|
||||
* 5. Dead/Removing → degraded:unhealthy
|
||||
* 6. Paused → paused:unknown
|
||||
* 7. Starting/Created → starting:unknown
|
||||
* 8. Exited → exited
|
||||
* 1. Degraded (from sub-resources) → degraded:unhealthy
|
||||
* 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true)
|
||||
* 3. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 4. Mixed (running + exited) → degraded:unhealthy
|
||||
* 5. Mixed (running + starting) → starting:unknown
|
||||
* 6. Running → running:healthy/unhealthy/unknown
|
||||
* 7. Dead/Removing → degraded:unhealthy
|
||||
* 8. Paused → paused:unknown
|
||||
* 9. Starting/Created → starting:unknown
|
||||
* 10. Exited → exited
|
||||
*
|
||||
* The $preserveRestarting parameter controls whether "restarting" containers should be
|
||||
* reported as "restarting:unknown" (true) or "degraded:unhealthy" (false, default).
|
||||
* - Use preserveRestarting=true for individual sub-resources (ServiceApplication/ServiceDatabase)
|
||||
* so they show "Restarting" in the UI.
|
||||
* - Use preserveRestarting=false for overall Service status aggregation where any restarting
|
||||
* container should mark the entire service as "Degraded".
|
||||
*/
|
||||
class ContainerStatusAggregator
|
||||
{
|
||||
|
|
@ -32,9 +41,10 @@ class ContainerStatusAggregator
|
|||
*
|
||||
* @param Collection $containerStatuses Collection of status strings (e.g., "running (healthy)", "running:healthy")
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0): string
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -64,10 +74,16 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting = false;
|
||||
$hasPaused = false;
|
||||
$hasDead = false;
|
||||
$hasDegraded = false;
|
||||
|
||||
// Parse each status string and set flags
|
||||
foreach ($containerStatuses as $status) {
|
||||
if (str($status)->contains('restarting')) {
|
||||
if (str($status)->contains('degraded')) {
|
||||
$hasDegraded = true;
|
||||
if (str($status)->contains('unhealthy')) {
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
} elseif (str($status)->contains('restarting')) {
|
||||
$hasRestarting = true;
|
||||
} elseif (str($status)->contains('running')) {
|
||||
$hasRunning = true;
|
||||
|
|
@ -98,7 +114,9 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
$hasDegraded,
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +125,10 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
*
|
||||
* @param Collection $containers Collection of Docker container objects with State property
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0): string
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -175,7 +194,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
false, // $hasDegraded - not applicable for container objects, only for status strings
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +211,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
* @param bool $hasStarting Has at least one starting/created container
|
||||
* @param bool $hasPaused Has at least one paused container
|
||||
* @param bool $hasDead Has at least one dead/removing container
|
||||
* @param bool $hasDegraded Has at least one degraded container
|
||||
* @param int $maxRestartCount Maximum restart count (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, return "restarting:unknown" instead of "degraded:unhealthy" for restarting containers
|
||||
* @return string Status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
private function resolveStatus(
|
||||
|
|
@ -202,24 +225,40 @@ private function resolveStatus(
|
|||
bool $hasStarting,
|
||||
bool $hasPaused,
|
||||
bool $hasDead,
|
||||
int $maxRestartCount
|
||||
bool $hasDegraded,
|
||||
int $maxRestartCount,
|
||||
bool $preserveRestarting = false
|
||||
): string {
|
||||
// Priority 1: Restarting containers (degraded state)
|
||||
if ($hasRestarting) {
|
||||
// Priority 1: Degraded containers from sub-resources (highest priority)
|
||||
// If any service/application within a service stack is degraded, the entire stack is degraded
|
||||
if ($hasDegraded) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 2: Crash loop detection (exited with restart count > 0)
|
||||
// Priority 2: Restarting containers
|
||||
// When preserveRestarting is true (for individual sub-resources), keep as "restarting"
|
||||
// When false (for overall service status), mark as "degraded"
|
||||
if ($hasRestarting) {
|
||||
return $preserveRestarting ? 'restarting:unknown' : 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Crash loop detection (exited with restart count > 0)
|
||||
if ($hasExited && $maxRestartCount > 0) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Mixed state (some running, some exited = degraded)
|
||||
// Priority 4: Mixed state (some running, some exited = degraded)
|
||||
if ($hasRunning && $hasExited) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 4: Running containers (check health status)
|
||||
// Priority 5: Mixed state (some running, some starting = still starting)
|
||||
// If any component is still starting, the entire service stack is not fully ready
|
||||
if ($hasRunning && $hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 6: Running containers (check health status)
|
||||
if ($hasRunning) {
|
||||
if ($hasUnhealthy) {
|
||||
return 'running:unhealthy';
|
||||
|
|
@ -230,22 +269,22 @@ private function resolveStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Priority 5: Dead or removing containers
|
||||
// Priority 7: Dead or removing containers
|
||||
if ($hasDead) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 6: Paused containers
|
||||
// Priority 8: Paused containers
|
||||
if ($hasPaused) {
|
||||
return 'paused:unknown';
|
||||
}
|
||||
|
||||
// Priority 7: Starting/created containers
|
||||
// Priority 9: Starting/created containers
|
||||
if ($hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 8: All containers exited (no restart count = truly stopped)
|
||||
// Priority 10: All containers exited (no restart count = truly stopped)
|
||||
return 'exited';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,10 +68,16 @@ function queue_application_deployment(Application $application, string $deployme
|
|||
]);
|
||||
|
||||
if ($no_questions_asked) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
);
|
||||
} elseif (next_queuable($server_id, $application_id, $commit, $pull_request_id)) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
'influxdb',
|
||||
'clickhouse/clickhouse-server',
|
||||
'timescaledb/timescaledb',
|
||||
'timescaledb', // Matches timescale/timescaledb
|
||||
'timescaledb-ha', // Matches timescale/timescaledb-ha
|
||||
'pgvector/pgvector',
|
||||
];
|
||||
const SPECIFIC_SERVICES = [
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
'ghcr.io/coollabsio/minio',
|
||||
'coollabsio/minio',
|
||||
'svhd/logto',
|
||||
'dxflrs/garage',
|
||||
];
|
||||
|
||||
// Based on /etc/os-release
|
||||
|
|
|
|||
|
|
@ -312,6 +312,36 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
|||
$LOGTO_ADMIN_ENDPOINT->value.':3002',
|
||||
]);
|
||||
break;
|
||||
case $type?->contains('garage'):
|
||||
$GARAGE_S3_API_URL = $variables->where('key', 'GARAGE_S3_API_URL')->first();
|
||||
$GARAGE_WEB_URL = $variables->where('key', 'GARAGE_WEB_URL')->first();
|
||||
$GARAGE_ADMIN_URL = $variables->where('key', 'GARAGE_ADMIN_URL')->first();
|
||||
|
||||
if (is_null($GARAGE_S3_API_URL) || is_null($GARAGE_WEB_URL) || is_null($GARAGE_ADMIN_URL)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
if (str($GARAGE_S3_API_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_S3_API_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 's3-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
if (str($GARAGE_WEB_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_WEB_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 'web-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
if (str($GARAGE_ADMIN_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_ADMIN_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 'admin-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
$payload = collect([
|
||||
$GARAGE_S3_API_URL->value.':3900',
|
||||
$GARAGE_WEB_URL->value.':3902',
|
||||
$GARAGE_ADMIN_URL->value.':3903',
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
|
|
@ -770,10 +800,26 @@ function isDatabaseImage(?string $image = null, ?array $serviceConfig = null)
|
|||
}
|
||||
$imageName = $image->before(':');
|
||||
|
||||
// First check if it's a known database image
|
||||
// Extract base image name (ignore registry prefix)
|
||||
// Examples:
|
||||
// docker.io/library/postgres -> postgres
|
||||
// ghcr.io/postgrest/postgrest -> postgrest
|
||||
// postgres -> postgres
|
||||
// postgrest/postgrest -> postgrest
|
||||
$baseImageName = $imageName;
|
||||
if (str($imageName)->contains('/')) {
|
||||
$baseImageName = str($imageName)->afterLast('/');
|
||||
}
|
||||
|
||||
// Check if base image name exactly matches a known database image
|
||||
$isKnownDatabase = false;
|
||||
foreach (DATABASE_DOCKER_IMAGES as $database_docker_image) {
|
||||
if (str($imageName)->contains($database_docker_image)) {
|
||||
// Extract base name from database pattern for comparison
|
||||
$databaseBaseName = str($database_docker_image)->contains('/')
|
||||
? str($database_docker_image)->afterLast('/')
|
||||
: $database_docker_image;
|
||||
|
||||
if ($baseImageName == $databaseBaseName) {
|
||||
$isKnownDatabase = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -1376,3 +1422,62 @@ function injectDockerComposeFlags(string $command, string $composeFilePath, stri
|
|||
// Replace only first occurrence to avoid modifying comments/strings/chained commands
|
||||
return preg_replace('/docker\s+compose/', $dockerComposeReplacement, $command, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject build arguments right after build-related subcommands in docker/docker compose commands.
|
||||
* This ensures build args are only applied to build operations, not to push, pull, up, etc.
|
||||
*
|
||||
* Supports:
|
||||
* - docker compose build
|
||||
* - docker buildx build
|
||||
* - docker builder build
|
||||
* - docker build (legacy)
|
||||
*
|
||||
* Examples:
|
||||
* - Input: "docker compose -f file.yml build"
|
||||
* Output: "docker compose -f file.yml build --build-arg X --build-arg Y"
|
||||
*
|
||||
* - Input: "docker buildx build --platform linux/amd64"
|
||||
* Output: "docker buildx build --build-arg X --build-arg Y --platform linux/amd64"
|
||||
*
|
||||
* - Input: "docker builder build --tag myimage:latest"
|
||||
* Output: "docker builder build --build-arg X --build-arg Y --tag myimage:latest"
|
||||
*
|
||||
* - Input: "docker compose build && docker compose push"
|
||||
* Output: "docker compose build --build-arg X --build-arg Y && docker compose push"
|
||||
*
|
||||
* - Input: "docker compose push"
|
||||
* Output: "docker compose push" (unchanged - no build command found)
|
||||
*
|
||||
* @param string $command The docker command
|
||||
* @param string $buildArgsString The build arguments to inject (e.g., "--build-arg X --build-arg Y")
|
||||
* @return string The modified command with build args injected after build subcommand
|
||||
*/
|
||||
function injectDockerComposeBuildArgs(string $command, string $buildArgsString): string
|
||||
{
|
||||
// Early return if no build args to inject
|
||||
if (empty(trim($buildArgsString))) {
|
||||
return $command;
|
||||
}
|
||||
|
||||
// Match build-related commands:
|
||||
// - ' builder build' (docker builder build)
|
||||
// - ' buildx build' (docker buildx build)
|
||||
// - ' build' (docker compose build, docker build)
|
||||
// Followed by either:
|
||||
// - whitespace (allowing service names, flags, or any valid arguments)
|
||||
// - end of string ($)
|
||||
// This regex ensures we match build subcommands, not "build" in other contexts
|
||||
// IMPORTANT: Order matters - check longer patterns first (builder build, buildx build) before ' build'
|
||||
$pattern = '/( builder build| buildx build| build)(?=\s|$)/';
|
||||
|
||||
// Replace the first occurrence of build command with build command + build-args
|
||||
$modifiedCommand = preg_replace(
|
||||
$pattern,
|
||||
'$1 '.$buildArgsString,
|
||||
$command,
|
||||
1 // Only replace first occurrence
|
||||
);
|
||||
|
||||
return $modifiedCommand ?? $command;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ function parseDockerVolumeString(string $volumeString): array
|
|||
];
|
||||
}
|
||||
|
||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
|
||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null): Collection
|
||||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
|
|
@ -1324,6 +1324,20 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
->values();
|
||||
|
||||
$payload['env_file'] = $envFiles;
|
||||
|
||||
// Inject commit-based image tag for services with build directive (for rollback support)
|
||||
// Only inject if service has build but no explicit image defined
|
||||
$hasBuild = data_get($service, 'build') !== null;
|
||||
$hasImage = data_get($service, 'image') !== null;
|
||||
if ($hasBuild && ! $hasImage && $commit) {
|
||||
$imageTag = str($commit)->substr(0, 128)->value();
|
||||
if ($isPullRequest) {
|
||||
$imageTag = "pr-{$pullRequestId}";
|
||||
}
|
||||
$imageRepo = "{$uuid}_{$serviceName}";
|
||||
$payload['image'] = "{$imageRepo}:{$imageTag}";
|
||||
}
|
||||
|
||||
if ($isPullRequest) {
|
||||
$serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ function () use ($server, $command_string) {
|
|||
);
|
||||
}
|
||||
|
||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false, ?int $timeout = null, bool $disableMultiplexing = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
|
||||
|
|
@ -126,11 +126,12 @@ function instant_remote_process(Collection|array $command, Server $server, bool
|
|||
$command = parseCommandsByLineForSudo(collect($command), $server);
|
||||
}
|
||||
$command_string = implode("\n", $command);
|
||||
$effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout');
|
||||
|
||||
return \App\Helpers\SshRetryHandler::retry(
|
||||
function () use ($server, $command_string) {
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
|
||||
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
|
||||
function () use ($server, $command_string, $effectiveTimeout, $disableMultiplexing) {
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing);
|
||||
$process = Process::timeout($effectiveTimeout)->run($sshCommand);
|
||||
|
||||
$output = trim($process->output());
|
||||
$exitCode = $process->exitCode();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.452',
|
||||
'version' => '4.0.0-beta.453',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -35,13 +35,6 @@
|
|||
'throw' => false,
|
||||
],
|
||||
|
||||
'webhooks-during-maintenance' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/webhooks-during-maintenance'),
|
||||
'visibility' => 'private',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->integer('docker_images_to_keep')->default(2);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_images_to_keep');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('disable_application_image_retention')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_application_image_retention');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -11,7 +11,6 @@ services:
|
|||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||
- /data/coolify/services:/var/www/html/storage/app/services
|
||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ services:
|
|||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -75,13 +74,7 @@ services:
|
|||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: "${DB_DATABASE:-coolify}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USERNAME}",
|
||||
"-d",
|
||||
"${DB_DATABASE:-coolify}"
|
||||
]
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
@ -121,7 +114,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ services:
|
|||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||
- /data/coolify/services:/var/www/html/storage/app/services
|
||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ services:
|
|||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -75,13 +74,7 @@ services:
|
|||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: "${DB_DATABASE:-coolify}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USERNAME}",
|
||||
"-d",
|
||||
"${DB_DATABASE:-coolify}"
|
||||
]
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
@ -121,7 +114,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.452"
|
||||
"version": "4.0.0-beta.453"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.453"
|
||||
"version": "4.0.0-beta.454"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
|
|
|||
BIN
public/svgs/fizzy.png
Normal file
BIN
public/svgs/fizzy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
118
public/svgs/garage.svg
Normal file
118
public/svgs/garage.svg
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="30mm"
|
||||
height="30mm"
|
||||
viewBox="0 0 30 30"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-47.531142,-150.58196)">
|
||||
<g
|
||||
id="g2446"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,27.649536,132.01223)">
|
||||
<g
|
||||
id="g6567"
|
||||
transform="matrix(0.92473907,0,0,0.92473907,11.032718,11.165159)">
|
||||
<g
|
||||
id="g7383"
|
||||
transform="matrix(1.0300991,0,0,1.0300991,3.770254,-1.2763086)">
|
||||
<path
|
||||
id="path6"
|
||||
d="m 136.06214,99.13643 c -0.8681,0.09646 -1.83266,0 -2.70078,-0.289369 L 99.794436,89.780144 c -0.868109,-0.28937 -1.736218,-0.675196 -2.507872,-1.157479 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path8"
|
||||
class="st0"
|
||||
d="m 85.036565,156.14226 c 1.919127,0.0226 3.842264,-0.048 5.758577,0.0407 1.109916,0.0647 2.081695,0.96893 2.125517,2.09821 0.05763,2.83895 0.0096,5.68171 0.02535,8.52216 0.0387,0.72125 -1.165534,0.55433 -1.656924,0.86227 -2.84639,0.78316 -5.867198,1.08468 -8.793555,0.62567 -2.484003,-0.4206 -4.607002,-2.18507 -5.651194,-4.45399 -1.332604,-2.83308 -1.546544,-6.07759 -1.21852,-9.15366 0.293175,-2.57048 1.448442,-5.0874 3.473195,-6.74732 2.184175,-1.91934 5.23662,-2.62252 8.078891,-2.19703 2.061965,0.25939 4.063024,1.01333 5.768107,2.20419 -0.194486,1.20116 -0.887464,2.34273 -1.929135,2.99015 -1.865545,-1.36891 -4.253598,-2.12198 -6.568068,-1.87184 -2.02236,0.3166 -3.762605,1.87404 -4.283558,3.85841 -0.666251,2.35645 -0.668458,4.88015 -0.252316,7.28143 0.337055,1.92315 1.48217,3.89047 3.44592,4.49149 1.860151,0.60901 3.846702,0.22762 5.72889,-0.0627 0.02323,-1.64043 -0.05713,-3.28547 0.06461,-4.92211 0.04478,-0.38456 -0.694745,-0.10524 -1.004029,-0.19009 -1.009365,-0.0553 -2.115945,0.1939 -3.015314,-0.38583 -0.860219,-0.80391 -0.327291,-2.03804 -0.09646,-2.99015 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path10"
|
||||
class="st0"
|
||||
d="m 109.82594,166.17374 c -0.0965,0.38583 -0.28937,0.77165 -0.57875,1.15748 -0.19291,0.38583 -0.48228,0.6752 -0.77164,0.86811 -1.25394,-0.0965 -2.31497,-0.77165 -2.99017,-1.92913 -1.15748,1.25393 -2.89369,2.02559 -4.62991,2.02559 -1.639774,0 -2.893709,-0.48229 -3.76182,-1.44685 -0.771653,-0.96457 -1.253937,-2.12205 -1.253937,-3.37598 0,-1.83268 0.57874,-3.18307 1.736221,-4.05118 1.350393,-0.96456 2.893706,-1.44685 4.533456,-1.35039 0.96458,0 1.92914,0 2.79726,0.0965 v -0.96457 c 0,-1.73622 -0.77166,-2.50787 -2.41143,-2.50787 -1.15747,0 -2.797241,0.38583 -4.919286,1.15748 -0.675197,-0.77165 -1.061024,-1.83268 -1.061024,-2.8937 2.218503,-0.96456 4.53347,-1.44685 6.94488,-1.44685 1.44686,-0.0965 2.79725,0.38583 3.95473,1.3504 0.96457,0.86811 1.5433,2.2185 1.5433,4.05117 v 6.55905 c -0.0965,1.44685 0.19291,2.2185 0.86812,2.70078 z m -8.10237,-0.77165 c 1.25394,-0.0965 2.41142,-0.57874 3.18308,-1.54331 v -2.79724 c -0.77166,-0.0965 -1.63977,-0.0965 -2.41143,-0.0965 -0.77165,-0.0965 -1.44684,0.19291 -2.02558,0.67519 -0.482291,0.48229 -0.675204,1.06103 -0.675204,1.73622 0,0.57874 0.192913,1.15748 0.578744,1.63976 0.38583,0.19292 0.86811,0.38583 1.35039,0.38583 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path12"
|
||||
class="st0"
|
||||
d="m 112.43026,153.92376 c 0.0965,-0.38583 0.28937,-0.77165 0.57874,-1.15748 0.19292,-0.38583 0.48229,-0.6752 0.77165,-0.86811 1.63976,0.19291 2.8937,1.35039 3.37599,2.8937 0.86811,-1.92913 2.2185,-2.8937 4.14764,-2.8937 0.57874,0 1.25392,0.0965 1.83267,0.19291 0,1.3504 -0.28937,2.60433 -0.96456,3.76181 -0.48229,-0.0965 -0.96457,-0.19291 -1.44685,-0.19291 -1.3504,0 -2.31496,0.67519 -3.18308,2.12204 v 10.2244 c -0.67519,0.0965 -1.35039,0.19291 -1.92914,0.19291 -0.67518,0 -1.35038,-0.0965 -2.02558,-0.19291 v -10.80314 c 0,-1.5433 -0.38582,-2.60433 -1.15748,-3.27952 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path14"
|
||||
class="st0"
|
||||
d="m 138.08774,166.17374 c -0.0965,0.38583 -0.28937,0.77165 -0.57874,1.15748 -0.19291,0.38583 -0.48228,0.6752 -0.77165,0.86811 -1.25394,-0.0965 -2.31496,-0.77165 -2.99017,-1.92913 -1.15747,1.25393 -2.89369,2.02559 -4.62992,2.02559 -1.63975,0 -2.8937,-0.48229 -3.7618,-1.44685 -0.77166,-0.96457 -1.25394,-2.12205 -1.25394,-3.37598 0,-1.83268 0.57874,-3.18307 1.73622,-4.05118 1.25393,-0.96456 2.8937,-1.44685 4.43701,-1.35039 0.96456,0 1.92914,0 2.79724,0.0965 v -0.96457 c 0,-1.73622 -0.77164,-2.50787 -2.41142,-2.50787 -1.15748,0 -2.79724,0.38583 -4.91929,1.15748 -0.6752,-0.77165 -1.06102,-1.83268 -1.06102,-2.8937 2.2185,-0.96456 4.53346,-1.44685 6.94488,-1.44685 1.44685,-0.0965 2.79725,0.38583 3.95473,1.3504 0.96456,0.86811 1.5433,2.2185 1.5433,4.05117 v 6.55905 c 0,1.44685 0.38583,2.2185 0.96457,2.70078 z m -8.10236,-0.77165 c 1.25393,-0.0965 2.41142,-0.57874 3.18307,-1.54331 v -2.79724 c -0.77165,-0.0965 -1.63977,-0.0965 -2.41142,-0.0965 -0.77165,-0.0965 -1.44686,0.19291 -2.02559,0.67519 -0.48228,0.48229 -0.67519,1.06103 -0.67519,1.73622 0,0.57874 0.19291,1.15748 0.57874,1.63976 0.38582,0.19292 0.8681,0.38583 1.35039,0.38583 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path16"
|
||||
class="st0"
|
||||
d="m 142.04247,166.07729 c -0.96457,-1.44685 -1.44686,-3.47244 -1.44686,-6.07677 0,-2.60433 0.57875,-4.62991 1.83268,-6.07676 1.06103,-1.35039 2.70079,-2.2185 4.43701,-2.2185 1.63977,0 3.18307,0.57874 4.34055,1.63976 0.57874,-0.77165 1.54332,-1.25394 2.50787,-1.35039 0.38583,0.19291 0.6752,0.57874 0.86812,0.86811 0.19291,0.38582 0.38583,0.67519 0.57874,1.15747 -0.57874,0.48229 -0.86812,1.44685 -0.86812,2.79725 v 9.06691 c 0,3.37598 -0.57874,5.7874 -1.63975,7.23424 -1.06103,1.44685 -2.99017,2.12205 -5.49804,2.12205 -1.92914,0 -3.95472,-0.38583 -5.7874,-1.06102 0,-1.06103 0.28937,-2.12205 0.96457,-2.8937 1.35039,0.6752 2.79724,0.96457 4.34054,0.96457 1.44686,0 2.41143,-0.38583 2.89371,-1.06103 0.57874,-0.86811 0.86811,-1.92913 0.77165,-2.99015 v -1.25394 c -1.15748,0.96457 -2.50787,1.54331 -4.05118,1.54331 -1.73622,-0.0965 -3.37599,-0.96457 -4.24409,-2.41141 z m 8.19882,-2.60433 v -7.42716 c -0.6752,-0.77165 -1.73622,-1.25393 -2.79725,-1.35039 -0.86811,0 -1.73621,0.57874 -2.12205,1.35039 -0.57874,1.25394 -0.86811,2.60433 -0.77165,3.95472 0,1.73622 0.19291,2.99016 0.67519,3.76181 0.28938,0.67519 1.06103,1.15748 1.83268,1.25393 1.3504,0 2.50788,-0.57874 3.18308,-1.5433 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path26"
|
||||
class="st3"
|
||||
d="m 136.73735,113.02618 18.42323,-7.42716 c 0.38583,-0.19291 0.57874,-0.57874 0.48228,-1.06102 -0.0965,-0.19292 -0.19291,-0.38583 -0.48228,-0.48229 -2.12204,-0.8681 -4.82284,-1.92913 -7.42716,-2.99015 -0.4823,-0.19291 -5.01576,3.08661 -5.40158,3.37598 l -7.90945,6.36613 c -1.83268,1.73622 -0.19291,3.27953 2.31496,2.21851 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<ellipse
|
||||
id="circle28"
|
||||
class="st3"
|
||||
cx="123.42634"
|
||||
cy="120.26041"
|
||||
rx="9.645668"
|
||||
ry="9.6456566"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path6-0"
|
||||
d="m 136.06214,99.13643 c -0.8681,0.09646 -1.83266,0 -2.70078,-0.289369 L 99.794436,89.780144 c -0.868109,-0.28937 -1.736218,-0.675196 -2.507872,-1.157479 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path18-7"
|
||||
class="st0"
|
||||
d="m 170.6901,161.35091 h -8.97047 c 0,1.06103 0.28937,2.02559 0.86811,2.8937 0.48228,0.6752 1.35039,1.06102 2.60432,1.06102 1.44686,-0.0965 2.89371,-0.48228 4.2441,-1.15748 0.6752,0.6752 1.06102,1.54331 1.15748,2.41142 -1.83267,1.25393 -3.95472,1.92913 -6.17323,1.83267 -2.41141,0 -4.14764,-0.77165 -5.20865,-2.31495 -1.06104,-1.54331 -1.54331,-3.5689 -1.54331,-6.07677 0,-2.50787 0.57873,-4.53346 1.73622,-6.07676 1.15747,-1.54331 2.99015,-2.41142 4.91928,-2.31496 2.12206,0 3.76182,0.6752 4.9193,1.92913 1.15748,1.35039 1.83267,3.08661 1.73622,4.91929 0,0.96456 -0.0965,1.92913 -0.28937,2.89369 z m -6.17323,-6.84841 c -1.73622,0 -2.70079,1.35039 -2.79724,3.95472 h 5.59448 v -0.38583 c 0,-0.86811 -0.19292,-1.83267 -0.67519,-2.60433 -0.48228,-0.67519 -1.3504,-0.96456 -2.12205,-0.96456 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path24-3-6-9"
|
||||
class="st4"
|
||||
d="m 123.0405,70.199461 c -1.44685,0 -2.89371,0.28937 -4.14765,0.868109 L 76.259006,89.973057 c -0.771652,0.289369 -1.157479,1.253935 -0.868109,2.025588 0,0 0,0 0,0 0,0.09646 0,0.09646 0.09646,0.192913 l 6.848424,13.503922 h 5.980314 l -0.86811,-4.72638 c -0.09646,-0.38582 -0.675197,-3.086605 -1.253937,-5.015736 l 19.966532,6.269676 c 0.28937,1.25394 0.57874,2.41141 1.06103,3.47244 h 32.31298 c 0.38582,-1.06103 0.67519,-2.2185 0.86811,-3.47244 l 19.87007,-6.17322 c -0.57873,1.929131 -1.15747,4.62992 -1.25393,5.01574 l -0.86812,4.72637 h 5.98032 l 6.75197,-13.407459 0.0965,-0.09646 0.0965,-0.192913 c 0,0 0,0 0,0 0.0965,-0.192913 0.0965,-0.28937 0.0965,-0.482283 0,-0.675196 -0.38583,-1.253935 -0.96457,-1.543305 l -42.6339,-18.905486 c -1.54332,-0.675196 -2.99017,-1.061022 -4.53347,-0.964566 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path24-3-2"
|
||||
class="st0"
|
||||
d="m 123.0405,79.073465 c -1.44685,0 -2.89371,0.28937 -4.14765,0.868109 L 76.259006,98.847061 c -0.771652,0.289369 -1.157479,1.253939 -0.868109,2.025589 0,0 0,0 0,0 0,0.0965 0,0.0965 0.09646,0.19291 l 3.665353,7.3307 h 7.909449 c -0.289371,-1.06102 -0.578742,-2.31496 -0.964568,-3.56889 l 11.285433,3.56889 h 51.507866 l 11.28542,-3.56889 c -0.38581,1.15748 -0.67518,2.50787 -0.96455,3.56889 h 7.90943 l 3.66536,-7.23424 0.0965,-0.0965 0.0965,-0.19291 c 0,0 0,0 0,0 0.0965,-0.19291 0.0965,-0.28937 0.0965,-0.48228 0,-0.6752 -0.38582,-1.25394 -0.96457,-1.543309 L 127.47751,79.941574 c -1.44686,-0.578739 -2.89371,-0.868109 -4.43701,-0.868109 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path24-0"
|
||||
class="st4"
|
||||
d="m 171.07592,109.45728 c 0,0.19292 0,0.28937 -0.0965,0.48229 0,0 0,0 0,0 l -0.0965,0.19291 v 0 l -0.0965,0.0965 -10.32087,20.44879 c -1.44684,2.79724 -4.05116,2.70078 -3.66533,-0.0965 l 2.12203,-11.57479 c 0.0965,-0.38582 0.6752,-3.08661 1.25394,-5.01574 l -19.87014,6.17322 c -3.08661,20.35234 -29.90156,20.64171 -34.24212,0 L 86.0974,113.89428 c 0.578741,1.92914 1.157481,4.62992 1.253938,5.01575 l 2.122046,11.57478 c 0.482284,2.8937 -2.218503,2.99016 -3.665353,0.0965 L 75.390897,110.03602 c 0,-0.0964 -0.09646,-0.0964 -0.09646,-0.19291 -0.385827,-0.77165 0,-1.73622 0.771653,-2.02559 0,0 0,0 0,0 l 42.63386,-18.905486 c 2.70078,-1.157478 5.88385,-1.157478 8.58464,0 l 42.63385,18.905486 c 0.77166,0.38583 1.15748,0.96457 1.15748,1.63976 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<path
|
||||
id="path26-2"
|
||||
class="st0"
|
||||
d="m 136.73735,113.02618 18.42323,-7.42716 c 0.38583,-0.19291 0.57874,-0.57874 0.48228,-1.06102 -0.0965,-0.19292 -0.19291,-0.38583 -0.48228,-0.48229 -2.12204,-0.8681 -4.82284,-1.92913 -7.42716,-2.99015 -0.4823,-0.19291 -5.01576,3.08661 -5.40158,3.37598 l -7.90945,6.36613 c -1.83268,1.73622 -0.19291,3.27953 2.31496,2.21851 z"
|
||||
style="stroke-width:0.964566" />
|
||||
<ellipse
|
||||
id="circle28-3"
|
||||
class="st0"
|
||||
cx="123.42634"
|
||||
cy="120.26041"
|
||||
rx="9.645668"
|
||||
ry="9.6456566"
|
||||
style="stroke-width:0.964566" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2346">
|
||||
.st0{fill:#4E4E4E;}
|
||||
.st1{fill:#FFD952;}
|
||||
.st2{fill:#49C8FA;}
|
||||
.st3{fill:#45C8FF;}
|
||||
.st4{fill:#FF9329;}
|
||||
.st5{fill:#3B2100;}
|
||||
.st6{fill:#C3C3C3;}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
BIN
public/svgs/rustfs.png
Normal file
BIN
public/svgs/rustfs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
15
public/svgs/rustfs.svg
Normal file
15
public/svgs/rustfs.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<linearGradient id="rustGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E67E22"/>
|
||||
<stop offset="100%" style="stop-color:#D35400"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="24" width="112" height="80" rx="8" fill="url(#rustGrad)"/>
|
||||
<rect x="20" y="38" width="88" height="12" rx="3" fill="#FFF" opacity="0.9"/>
|
||||
<rect x="20" y="58" width="88" height="12" rx="3" fill="#FFF" opacity="0.7"/>
|
||||
<rect x="20" y="78" width="88" height="12" rx="3" fill="#FFF" opacity="0.5"/>
|
||||
<circle cx="28" cy="44" r="3" fill="#27AE60"/>
|
||||
<circle cx="28" cy="64" r="3" fill="#27AE60"/>
|
||||
<circle cx="28" cy="84" r="3" fill="#27AE60"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 753 B |
|
|
@ -185,4 +185,15 @@ .input[type="password"] {
|
|||
|
||||
.lds-heart {
|
||||
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
.log-highlight {
|
||||
background-color: rgba(234, 179, 8, 0.4);
|
||||
border-radius: 2px;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.dark .log-highlight {
|
||||
background-color: rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
|
|
@ -230,7 +230,7 @@ @utility box-without-bg-without-border {
|
|||
}
|
||||
|
||||
@utility coolbox {
|
||||
@apply relative flex transition-all duration-150 dark:bg-coolgray-100 bg-white p-2 rounded-lg border border-neutral-200 dark:border-coolgray-400 hover:ring-2 dark:hover:ring-warning hover:ring-coollabs cursor-pointer min-h-[4rem];
|
||||
@apply relative flex transition-all duration-150 dark:bg-coolgray-100 bg-white p-2 rounded border border-neutral-200 dark:border-coolgray-400 hover:ring-2 dark:hover:ring-warning hover:ring-coollabs cursor-pointer min-h-[4rem];
|
||||
}
|
||||
|
||||
@utility on-box {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
|
||||
@foreach ($servers as $server)
|
||||
@php
|
||||
$info = $server->outdatedInfo ?? [];
|
||||
$current = $info['current'] ?? 'unknown';
|
||||
$latest = $info['latest'] ?? 'unknown';
|
||||
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
|
||||
$serverName = data_get($server, 'name', 'Unknown Server');
|
||||
$serverUrl = data_get($server, 'url', '#');
|
||||
$info = data_get($server, 'outdatedInfo', []);
|
||||
$current = data_get($info, 'current', 'unknown');
|
||||
$latest = data_get($info, 'latest', 'unknown');
|
||||
$isPatch = (data_get($info, 'type', 'patch_update') === 'patch_update');
|
||||
$hasNewerBranch = isset($info['newer_branch_target']);
|
||||
$hasUpgrades = $hasUpgrades ?? false;
|
||||
if (!$isPatch || $hasNewerBranch) {
|
||||
|
|
@ -19,8 +21,9 @@
|
|||
$latest = str_starts_with($latest, 'v') ? $latest : "v{$latest}";
|
||||
|
||||
// For minor upgrades, use the upgrade_target (e.g., "v3.6")
|
||||
if (!$isPatch && isset($info['upgrade_target'])) {
|
||||
$upgradeTarget = str_starts_with($info['upgrade_target'], 'v') ? $info['upgrade_target'] : "v{$info['upgrade_target']}";
|
||||
if (!$isPatch && data_get($info, 'upgrade_target')) {
|
||||
$upgradeTarget = data_get($info, 'upgrade_target');
|
||||
$upgradeTarget = str_starts_with($upgradeTarget, 'v') ? $upgradeTarget : "v{$upgradeTarget}";
|
||||
} else {
|
||||
// For patch updates, show the full version
|
||||
$upgradeTarget = $latest;
|
||||
|
|
@ -28,22 +31,23 @@
|
|||
|
||||
// Get newer branch info if available
|
||||
if ($hasNewerBranch) {
|
||||
$newerBranchTarget = $info['newer_branch_target'];
|
||||
$newerBranchLatest = str_starts_with($info['newer_branch_latest'], 'v') ? $info['newer_branch_latest'] : "v{$info['newer_branch_latest']}";
|
||||
$newerBranchTarget = data_get($info, 'newer_branch_target', 'unknown');
|
||||
$newerBranchLatest = data_get($info, 'newer_branch_latest', 'unknown');
|
||||
$newerBranchLatest = str_starts_with($newerBranchLatest, 'v') ? $newerBranchLatest : "v{$newerBranchLatest}";
|
||||
}
|
||||
@endphp
|
||||
@if ($isPatch && $hasNewerBranch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available) | Also available: {{ $newerBranchTarget }} (latest patch: {{ $newerBranchLatest }}) - new minor version
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} → {{ $upgradeTarget }} (patch update available) | Also available: {{ $newerBranchTarget }} (latest patch: {{ $newerBranchLatest }}) - new minor version
|
||||
@elseif ($isPatch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available)
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} → {{ $upgradeTarget }} (patch update available)
|
||||
@else
|
||||
- **{{ $server->name }}**: {{ $current }} (latest patch: {{ $latest }}) → {{ $upgradeTarget }} (new minor version available)
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} (latest patch: {{ $latest }}) → {{ $upgradeTarget }} (new minor version available)
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
## Recommendation
|
||||
|
||||
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration through your [Coolify Dashboard]({{ config('app.url') }}).
|
||||
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration by clicking on any server name above.
|
||||
|
||||
@if ($hasUpgrades ?? false)
|
||||
**Important for minor version upgrades:** Before upgrading to a new minor version, please read the [Traefik changelog](https://github.com/traefik/traefik/releases) to understand breaking changes and new features.
|
||||
|
|
@ -58,5 +62,5 @@
|
|||
|
||||
---
|
||||
|
||||
You can manage your server proxy settings in your Coolify Dashboard.
|
||||
Click on any server name above to manage its proxy settings.
|
||||
</x-emails.layout>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
if (!html) return '';
|
||||
const URL_RE = /^(https?:|mailto:)/i;
|
||||
const config = {
|
||||
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'p', 'pre', 's', 'span', 'strong',
|
||||
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'mark', 'p', 'pre', 's', 'span', 'strong',
|
||||
'u'
|
||||
],
|
||||
ALLOWED_ATTR: ['class', 'href', 'target', 'title', 'rel'],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
<div class="flex items-center gap-2 pb-4">
|
||||
<h2>Deployment Log</h2>
|
||||
@if ($is_debug_enabled)
|
||||
<x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button>
|
||||
@else
|
||||
<x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button>
|
||||
@endif
|
||||
@if (isDev())
|
||||
<x-forms.button x-on:click="$wire.copyLogsToClipboard().then(text => navigator.clipboard.writeText(text))">Copy Logs</x-forms.button>
|
||||
@endif
|
||||
@if (data_get($application_deployment_queue, 'status') === 'queued')
|
||||
<x-forms.button wire:click.prevent="force_start">Force Start</x-forms.button>
|
||||
@endif
|
||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
|
||||
data_get($application_deployment_queue, 'status') === 'queued')
|
||||
@if (
|
||||
data_get($application_deployment_queue, 'status') === 'in_progress' ||
|
||||
data_get($application_deployment_queue, 'status') === 'queued'
|
||||
)
|
||||
<x-forms.button isError wire:click.prevent="cancel">Cancel</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
|
||||
</x-slot>
|
||||
<h1 class="py-0">Deployment</h1>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" />
|
||||
<div x-data="{
|
||||
</x-slot>
|
||||
<h1 class="py-0">Deployment</h1>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" />
|
||||
<div x-data="{
|
||||
fullscreen: false,
|
||||
alwaysScroll: false,
|
||||
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
|
||||
intervalId: null,
|
||||
showTimestamps: true,
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if (this.fullscreen === false) {
|
||||
|
|
@ -17,15 +20,16 @@
|
|||
clearInterval(this.intervalId);
|
||||
}
|
||||
},
|
||||
isScrolling: false,
|
||||
toggleScroll() {
|
||||
this.alwaysScroll = !this.alwaysScroll;
|
||||
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const screen = document.getElementById('screen');
|
||||
const logs = document.getElementById('logs');
|
||||
if (screen.scrollTop !== logs.scrollHeight) {
|
||||
screen.scrollTop = logs.scrollHeight;
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
setTimeout(() => { this.isScrolling = false; }, 50);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
|
|
@ -33,97 +37,257 @@
|
|||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
goTop() {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
const screen = document.getElementById('screen');
|
||||
screen.scrollTop = 0;
|
||||
handleScroll(event) {
|
||||
if (!this.alwaysScroll || this.isScrolling) return;
|
||||
const el = event.target;
|
||||
// Check if user scrolled away from the bottom
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (distanceFromBottom > 50) {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
matchesSearch(text) {
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return text.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
let decoded = text;
|
||||
let prev = '';
|
||||
let iterations = 0;
|
||||
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities
|
||||
|
||||
while (decoded !== prev && iterations < maxIterations) {
|
||||
prev = decoded;
|
||||
const doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||
decoded = doc.documentElement.textContent;
|
||||
iterations++;
|
||||
}
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
|
||||
if (!this.searchQuery.trim()) {
|
||||
el.textContent = decoded;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
const lowerText = decoded.toLowerCase();
|
||||
let lastIndex = 0;
|
||||
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
while (index !== -1) {
|
||||
// Add text before match
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
|
||||
}
|
||||
// Add highlighted match
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'log-highlight';
|
||||
mark.textContent = decoded.substring(index, index + this.searchQuery.length);
|
||||
el.appendChild(mark);
|
||||
|
||||
lastIndex = index + this.searchQuery.length;
|
||||
index = lowerText.indexOf(query, lastIndex);
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < decoded.length) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
|
||||
}
|
||||
},
|
||||
getMatchCount() {
|
||||
if (!this.searchQuery.trim()) return 0;
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return 0;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
let count = 0;
|
||||
lines.forEach(line => {
|
||||
if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
downloadLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
const visibleLines = logs.querySelectorAll('[data-log-line]:not(.hidden)');
|
||||
let content = '';
|
||||
visibleLines.forEach(line => {
|
||||
const text = line.textContent.replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
content += text + String.fromCharCode(10);
|
||||
}
|
||||
});
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0,19).replace(/[T:]/g, '-');
|
||||
a.download = 'deployment-' + this.deploymentId + '-' + timestamp + '.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
init() {
|
||||
// Re-render logs after Livewire updates
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
});
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
});
|
||||
});
|
||||
// Start auto-scroll if deployment is in progress
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
setTimeout(() => { this.isScrolling = false; }, 50);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}">
|
||||
<livewire:project.application.deployment-navbar :application_deployment_queue="$application_deployment_queue" />
|
||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
||||
<div class="flex items-center gap-1 pt-2 ">Deployment is
|
||||
<div class="dark:text-warning">
|
||||
{{ Str::headline(data_get($this->application_deployment_queue, 'status')) }}.
|
||||
</div>
|
||||
<x-loading class="loading-ring" />
|
||||
</div>
|
||||
{{-- <div class="">Logs will be updated automatically.</div> --}}
|
||||
@else
|
||||
<div class="pt-2 ">Deployment is <span
|
||||
class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>.
|
||||
</div>
|
||||
@endif
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen' : 'relative'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300"
|
||||
:class="fullscreen ? '' : 'min-h-14 max-h-[40rem] border border-dotted rounded-sm'">
|
||||
<div :class="fullscreen ? 'fixed' : 'absolute'" class="top-2 right-5">
|
||||
<div class="flex justify-end gap-4">
|
||||
<button title="Toggle timestamps" x-on:click="showTimestamps = !showTimestamps">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Go Top" x-show="fullscreen" x-on:click="goTop">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-10l-4-4M8 9l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'dark:text-warning' : ''"
|
||||
x-on:click="toggleScroll">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
<path fill="currentColor"
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100"
|
||||
viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<livewire:project.application.deployment-navbar
|
||||
:application_deployment_queue="$application_deployment_queue" />
|
||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
||||
<div class="flex items-center gap-1 pt-2 ">Deployment is
|
||||
<div class="dark:text-warning">
|
||||
{{ Str::headline(data_get($this->application_deployment_queue, 'status')) }}.
|
||||
</div>
|
||||
<x-loading class="loading-ring" />
|
||||
</div>
|
||||
|
||||
<div id="logs" class="flex flex-col font-mono">
|
||||
@forelse ($this->logLines as $line)
|
||||
<div @class([
|
||||
'mt-2' => isset($line['command']) && $line['command'],
|
||||
'flex gap-2 dark:hover:bg-coolgray-500 hover:bg-gray-100',
|
||||
])>
|
||||
<span x-show="showTimestamps" class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
||||
<span @class([
|
||||
'text-success dark:text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['stderr'],
|
||||
'font-bold' => isset($line['command']) && $line['command'],
|
||||
'whitespace-pre-wrap',
|
||||
])>{!! (isset($line['command']) && $line['command'] ? '[CMD]: ' : '') . trim($line['line']) !!}</span>
|
||||
{{-- <div class="">Logs will be updated automatically.</div> --}}
|
||||
@else
|
||||
<div class="pt-2 ">Deployment is <span
|
||||
class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>.
|
||||
</div>
|
||||
@endif
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'relative'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'mt-4 border border-dotted rounded-sm'">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
<span x-show="!searchQuery.trim()"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input type="text" x-model="searchQuery" placeholder="Find in logs"
|
||||
class="input input-sm w-48 pl-8 pr-8 dark:bg-coolgray-200" />
|
||||
<button x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Toggle Timestamps" x-on:click="showTimestamps = !showTimestamps"
|
||||
:class="showTimestamps ? '!text-warning' : ''"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:click="toggleDebug"
|
||||
title="{{ $is_debug_enabled ? 'Hide Debug Logs' : 'Show Debug Logs' }}"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $is_debug_enabled ? '!text-warning' : '' }}">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? '!text-warning' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
<path fill="currentColor"
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@empty
|
||||
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
<div id="logsContainer" @scroll="handleScroll"
|
||||
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[40rem]'">
|
||||
<div id="logs" class="flex flex-col font-mono">
|
||||
<div x-show="searchQuery.trim() && getMatchCount() === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
</div>
|
||||
@forelse ($this->logLines as $line)
|
||||
@php
|
||||
$lineContent = (isset($line['command']) && $line['command'] ? '[CMD]: ' : '') . trim($line['line']);
|
||||
$searchableContent = $line['timestamp'] . ' ' . $lineContent;
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
|
||||
x-bind:class="{ 'hidden': !matchesSearch($el.dataset.logContent) }" @class([
|
||||
'mt-2' => isset($line['command']) && $line['command'],
|
||||
'flex gap-2',
|
||||
])>
|
||||
<span x-show="showTimestamps"
|
||||
class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
||||
<span data-line-text="{{ htmlspecialchars($lineContent) }}" @class([
|
||||
'text-success dark:text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['stderr'],
|
||||
'font-bold' => isset($line['command']) && $line['command'],
|
||||
'whitespace-pre-wrap',
|
||||
])
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"></span>
|
||||
</div>
|
||||
@empty
|
||||
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -241,12 +241,32 @@
|
|||
@else
|
||||
<div class="flex flex-col gap-2">
|
||||
@endcan
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" id="baseDirectory"
|
||||
label="Base Directory" helper="Directory to use as root. Useful for monorepos." />
|
||||
<div x-data="{
|
||||
baseDir: '{{ $application->base_directory }}',
|
||||
composeLocation: '{{ $application->docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
path = path.replace(/\/+$/, '');
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeComposeLocation() {
|
||||
this.composeLocation = this.normalizePath(this.composeLocation);
|
||||
}
|
||||
}" class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" wire:model.defer="baseDirectory"
|
||||
label="Base Directory" helper="Directory to use as root. Useful for monorepos."
|
||||
x-model="baseDir" @blur="normalizeBaseDir()" />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/docker-compose.yaml"
|
||||
id="dockerComposeLocation" label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
|
||||
wire:model.defer="dockerComposeLocation" label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>"
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
|
||||
|
|
@ -293,13 +313,32 @@
|
|||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input placeholder="/" id="baseDirectory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate" />
|
||||
<div x-data="{
|
||||
baseDir: '{{ $application->base_directory }}',
|
||||
dockerfileLocation: '{{ $application->dockerfile_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
path = path.replace(/\/+$/, '');
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeDockerfileLocation() {
|
||||
this.dockerfileLocation = this.normalizePath(this.dockerfileLocation);
|
||||
}
|
||||
}" class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input placeholder="/" wire:model.defer="baseDirectory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate"
|
||||
x-model="baseDir" @blur="normalizeBaseDir()" />
|
||||
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
||||
<x-forms.input placeholder="/Dockerfile" id="dockerfileLocation" label="Dockerfile Location"
|
||||
<x-forms.input placeholder="/Dockerfile" wire:model.defer="dockerfileLocation" label="Dockerfile Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
x-bind:disabled="!canUpdate" x-model="dockerfileLocation" @blur="normalizeDockerfileLocation()" />
|
||||
@endif
|
||||
|
||||
@if ($application->build_pack === 'dockerfile')
|
||||
|
|
|
|||
|
|
@ -5,25 +5,51 @@
|
|||
<x-forms.button wire:click='loadImages(true)'>Reload Available Images</x-forms.button>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="pb-4 ">You can easily rollback to a previously built (local) images
|
||||
quickly.</div>
|
||||
<div class="pb-4">You can easily rollback to a previously built (local) images quickly.</div>
|
||||
|
||||
@if($serverRetentionDisabled)
|
||||
<x-callout type="warning" class="mb-4">
|
||||
Image retention is disabled at the server level. This setting has no effect until the server administrator enables it.
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="pb-4">
|
||||
<form wire:submit="saveSettings" class="flex items-end gap-2 w-96">
|
||||
<x-forms.input id="dockerImagesToKeep" type="number" min="0" max="100" label="Images to keep for rollback"
|
||||
helper="Number of Docker images to keep for rollback during cleanup. Set to 0 to only keep the currently running image. PR images are always deleted during cleanup.<br><br><strong>Note:</strong> Server administrators can disable image retention at the server level, which overrides this setting."
|
||||
canGate="update" :canResource="$application" :disabled="$serverRetentionDisabled" />
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit" :disabled="$serverRetentionDisabled">Save</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
<div wire:target='loadImages' wire:loading.remove>
|
||||
<div class="flex flex-wrap">
|
||||
@forelse ($images as $image)
|
||||
<div class="w-2/4 p-2">
|
||||
<div class="bg-white border rounded-sm dark:border-coolgray-300 dark:bg-coolgray-100 border-neutral-200">
|
||||
<div
|
||||
class="bg-white border rounded-sm dark:border-coolgray-300 dark:bg-coolgray-100 border-neutral-200">
|
||||
@php
|
||||
$tag = data_get($image, 'tag');
|
||||
$date = data_get($image, 'created_at');
|
||||
$interval = \Illuminate\Support\Carbon::parse($date);
|
||||
// Check if tag looks like a commit SHA (hex string) or PR tag (pr-N)
|
||||
$isCommitSha = preg_match('/^[0-9a-f]{7,128}$/i', $tag);
|
||||
$isPrTag = preg_match('/^pr-\d+$/', $tag);
|
||||
$isRollbackable = $isCommitSha || $isPrTag;
|
||||
@endphp
|
||||
<div class="p-2">
|
||||
<div class="">
|
||||
@if (data_get($image, 'is_current'))
|
||||
<span class="font-bold dark:text-warning">LIVE</span>
|
||||
|
|
||||
@endif
|
||||
SHA: {{ data_get($image, 'tag') }}
|
||||
@if ($isCommitSha)
|
||||
SHA: {{ $tag }}
|
||||
@elseif ($isPrTag)
|
||||
PR: {{ $tag }}
|
||||
@else
|
||||
Tag: {{ $tag }}
|
||||
@endif
|
||||
</div>
|
||||
@php
|
||||
$date = data_get($image, 'created_at');
|
||||
$interval = \Illuminate\Support\Carbon::parse($date);
|
||||
@endphp
|
||||
<div class="text-xs">{{ $interval->diffForHumans() }}</div>
|
||||
<div class="text-xs">{{ $date }}</div>
|
||||
</div>
|
||||
|
|
@ -33,9 +59,13 @@
|
|||
<x-forms.button disabled tooltip="This image is currently running.">
|
||||
Rollback
|
||||
</x-forms.button>
|
||||
@elseif (!$isRollbackable)
|
||||
<x-forms.button disabled tooltip="Rollback not available for '{{ $tag }}' tag. Only commit-based tags support rollback. Re-deploy to create a rollback-enabled image.">
|
||||
Rollback
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button class="dark:bg-coolgray-100"
|
||||
wire:click="rollbackImage('{{ data_get($image, 'tag') }}')">
|
||||
wire:click="rollbackImage('{{ $tag }}')">
|
||||
Rollback
|
||||
</x-forms.button>
|
||||
@endif
|
||||
|
|
@ -49,4 +79,4 @@
|
|||
</div>
|
||||
</div>
|
||||
<div wire:target='loadImages' wire:loading>Loading available docker images...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !$isPublic }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !$isPublic }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !$isPublic }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !$isPublic }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="flex flex-col justify-center gap-2 text-left ">
|
||||
@forelse ($private_keys as $key)
|
||||
@if ($private_key_id == $key->id)
|
||||
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-100 coolbox"
|
||||
<div class="gap-2 py-4 cursor-pointer group coolbox"
|
||||
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">
|
||||
|
|
@ -20,7 +20,7 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-100 coolbox"
|
||||
<div class="gap-2 py-4 cursor-pointer group coolbox"
|
||||
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">
|
||||
|
|
@ -61,12 +61,33 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location"
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeComposeLocation() {
|
||||
this.composeLocation = this.normalizePath(this.composeLocation);
|
||||
}
|
||||
}" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
|
||||
@blur="normalizeBaseDir()" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.defer="docker_compose_location"
|
||||
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span class='dark:text-warning'
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<div class="flex flex-col justify-center gap-2 text-left">
|
||||
@foreach ($github_apps as $ghapp)
|
||||
<div class="flex">
|
||||
<div class="w-full gap-2 py-4 bg-white cursor-pointer group hover:bg-coollabs dark:bg-coolgray-200 coolbox"
|
||||
<div class="w-full gap-2 py-4 group coolbox"
|
||||
wire:click.prevent="loadRepositories({{ $ghapp->id }})"
|
||||
wire:key="{{ $ghapp->id }}">
|
||||
<div class="flex mr-4">
|
||||
|
|
@ -95,15 +95,35 @@
|
|||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory"
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeComposeLocation() {
|
||||
this.composeLocation = this.normalizePath(this.composeLocation);
|
||||
}
|
||||
}" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory"
|
||||
label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos."
|
||||
x-model="baseDir" />
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
|
||||
@blur="normalizeBaseDir()" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.blur="docker_compose_location" label="Docker Compose Location"
|
||||
wire:model.defer="docker_compose_location" label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span
|
||||
|
|
|
|||
|
|
@ -52,12 +52,33 @@
|
|||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location"
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeComposeLocation() {
|
||||
this.composeLocation = this.normalizePath(this.composeLocation);
|
||||
}
|
||||
}" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
|
||||
@blur="normalizeBaseDir()" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.defer="docker_compose_location"
|
||||
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span class='dark:text-warning'
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ function searchResources() {
|
|||
PostgreSQL
|
||||
17 (default).</div>
|
||||
<div class="flex flex-col gap-6 pt-8">
|
||||
<div class="gap-2 border border-transparent box-without-bg dark:bg-coolgray-100 bg-white dark:hover:text-neutral-400 dark:hover:bg-coollabs group flex"
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:17-alpine'))"
|
||||
:disabled="selecting">
|
||||
|
|
@ -460,17 +460,18 @@ function searchResources() {
|
|||
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-6000"
|
||||
onclick="event.stopPropagation()" href="https://hub.docker.com/_/postgres/"
|
||||
target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://hub.docker.com/_/postgres/" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 border border-transparent box-without-bg dark:bg-coolgray-100 bg-white dark:hover:text-neutral-400 dark:hover:bg-coollabs group flex"
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('supabase/postgres:17.4.1.032'))"
|
||||
:disabled="selecting">
|
||||
|
|
@ -480,16 +481,18 @@ function searchResources() {
|
|||
Supabase is a modern, open-source alternative to PostgreSQL with lots of extensions.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" href="https://github.com/supabase/postgres"
|
||||
target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://github.com/supabase/postgres" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 border border-transparent box-without-bg dark:bg-coolgray-100 bg-white dark:hover:text-neutral-400 dark:hover:bg-coollabs group flex"
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgis/postgis:17-3.5-alpine'))"
|
||||
:disabled="selecting">
|
||||
|
|
@ -499,16 +502,18 @@ function searchResources() {
|
|||
PostGIS is a PostgreSQL extension for geographic objects.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" href="https://github.com/postgis/docker-postgis"
|
||||
target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://github.com/postgis/docker-postgis" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 border border-transparent box-without-bg dark:bg-coolgray-100 bg-white dark:hover:text-neutral-400 dark:hover:bg-coollabs group flex"
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('pgvector/pgvector:pg17'))"
|
||||
:disabled="selecting">
|
||||
|
|
@ -518,15 +523,16 @@ function searchResources() {
|
|||
PGVector is a PostgreSQL extension for vector data types.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" href="https://github.com/pgvector/pgvector"
|
||||
target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://github.com/pgvector/pgvector" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,16 @@
|
|||
<livewire:project.service.stack-form :service="$service" />
|
||||
<h3>Services</h3>
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
|
||||
@if($applications->isEmpty() && $databases->isEmpty())
|
||||
<div class="p-4 text-sm text-neutral-500">
|
||||
No services defined in this Docker Compose file.
|
||||
</div>
|
||||
@elseif($applications->isEmpty())
|
||||
<div class="p-4 text-sm text-neutral-500">
|
||||
No applications with domains defined. Only database services are available.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach ($applications as $application)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => str(
|
||||
|
|
|
|||
|
|
@ -56,18 +56,18 @@
|
|||
<h3 class="py-2 pt-4">Advanced</h3>
|
||||
<div class="w-96 flex flex-col gap-1">
|
||||
@if (str($application->image)->contains('pocketbase'))
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isGzipEnabled"
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="Pocketbase does not need gzip compression, otherwise SSE will not work." disabled />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isGzipEnabled"
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isStripprefixEnabled"
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isStripprefixEnabled"
|
||||
label="Strip Prefixes"
|
||||
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave label="Exclude from service status"
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$application"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
<div class="p-4 my-4 border dark:border-coolgray-200 border-neutral-200">
|
||||
<div x-init="$wire.getLogs" id="screen" x-data="{
|
||||
<div class="{{ $collapsible ? 'my-4 border dark:border-coolgray-200 border-neutral-200' : '' }}">
|
||||
<div id="screen" x-data="{
|
||||
collapsible: {{ $collapsible ? 'true' : 'false' }},
|
||||
expanded: {{ ($expandByDefault || !$collapsible) ? 'true' : 'false' }},
|
||||
logsLoaded: false,
|
||||
fullscreen: false,
|
||||
alwaysScroll: false,
|
||||
intervalId: null,
|
||||
colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
containerName: '{{ $container ?? "logs" }}',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if (this.fullscreen === false) {
|
||||
|
|
@ -10,15 +17,16 @@
|
|||
clearInterval(this.intervalId);
|
||||
}
|
||||
},
|
||||
isScrolling: false,
|
||||
toggleScroll() {
|
||||
this.alwaysScroll = !this.alwaysScroll;
|
||||
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const screen = document.getElementById('screen');
|
||||
const logs = document.getElementById('logs');
|
||||
if (screen.scrollTop !== logs.scrollHeight) {
|
||||
screen.scrollTop = logs.scrollHeight;
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
setTimeout(() => { this.isScrolling = false; }, 50);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
|
|
@ -26,63 +34,258 @@
|
|||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
goTop() {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
const screen = document.getElementById('screen');
|
||||
screen.scrollTop = 0;
|
||||
handleScroll(event) {
|
||||
if (!this.alwaysScroll || this.isScrolling) return;
|
||||
const el = event.target;
|
||||
// Check if user scrolled away from the bottom
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (distanceFromBottom > 50) {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
toggleColorLogs() {
|
||||
this.colorLogs = !this.colorLogs;
|
||||
localStorage.setItem('coolify-color-logs', this.colorLogs);
|
||||
},
|
||||
getLogLevel(text) {
|
||||
const lowerText = text.toLowerCase();
|
||||
// Error detection (highest priority)
|
||||
if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(lowerText)) {
|
||||
return 'error';
|
||||
}
|
||||
// Warning detection
|
||||
if (/\b(warn|warning|wrn|caution)\b/.test(lowerText)) {
|
||||
return 'warning';
|
||||
}
|
||||
// Debug detection
|
||||
if (/\b(debug|dbg|trace|verbose)\b/.test(lowerText)) {
|
||||
return 'debug';
|
||||
}
|
||||
// Info detection
|
||||
if (/\b(info|inf|notice)\b/.test(lowerText)) {
|
||||
return 'info';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
matchesSearch(line) {
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return line.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
let decoded = text;
|
||||
let prev = '';
|
||||
let iterations = 0;
|
||||
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities
|
||||
|
||||
while (decoded !== prev && iterations < maxIterations) {
|
||||
prev = decoded;
|
||||
const doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||
decoded = doc.documentElement.textContent;
|
||||
iterations++;
|
||||
}
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
|
||||
if (!this.searchQuery.trim()) {
|
||||
el.textContent = decoded;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
const lowerText = decoded.toLowerCase();
|
||||
let lastIndex = 0;
|
||||
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
while (index !== -1) {
|
||||
// Add text before match
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
|
||||
}
|
||||
// Add highlighted match
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'log-highlight';
|
||||
mark.textContent = decoded.substring(index, index + this.searchQuery.length);
|
||||
el.appendChild(mark);
|
||||
|
||||
lastIndex = index + this.searchQuery.length;
|
||||
index = lowerText.indexOf(query, lastIndex);
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < decoded.length) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
|
||||
}
|
||||
},
|
||||
getMatchCount() {
|
||||
if (!this.searchQuery.trim()) return 0;
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return 0;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
let count = 0;
|
||||
lines.forEach(line => {
|
||||
if (line.textContent.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
downloadLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
const visibleLines = logs.querySelectorAll('[data-log-line]:not(.hidden)');
|
||||
let content = '';
|
||||
visibleLines.forEach(line => {
|
||||
const text = line.textContent.replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
content += text + String.fromCharCode(10);
|
||||
}
|
||||
});
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0,19).replace(/[T:]/g, '-');
|
||||
a.download = this.containerName + '-logs-' + timestamp + '.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
init() {
|
||||
if (this.expanded) {
|
||||
this.$wire.getLogs();
|
||||
this.logsLoaded = true;
|
||||
}
|
||||
// Re-render logs after Livewire updates
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
});
|
||||
});
|
||||
}
|
||||
}">
|
||||
<div class="flex gap-2 items-center">
|
||||
@if ($displayName)
|
||||
<h4>{{ $displayName }}</h4>
|
||||
@elseif ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))
|
||||
<h4>{{ $container }}</h4>
|
||||
@else
|
||||
<h4>{{ str($container)->beforeLast('-')->headline() }}</h4>
|
||||
@endif
|
||||
@if ($pull_request)
|
||||
<div>({{ $pull_request }})</div>
|
||||
@endif
|
||||
@if ($streamLogs)
|
||||
<x-loading wire:poll.2000ms='getLogs(true)' />
|
||||
@endif
|
||||
</div>
|
||||
<form wire:submit='getLogs(true)' class="flex flex-col gap-4">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="100" type="number" required
|
||||
id="numberOfLines" :readonly="$streamLogs"></x-forms.input>
|
||||
@if ($collapsible)
|
||||
<div class="flex gap-2 items-center p-4 cursor-pointer select-none hover:bg-gray-50 dark:hover:bg-coolgray-200"
|
||||
x-on:click="expanded = !expanded; if (expanded && !logsLoaded) { $wire.getLogs(); logsLoaded = true; }">
|
||||
<svg class="w-4 h-4 transition-transform" :class="expanded ? 'rotate-90' : ''" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
|
||||
</svg>
|
||||
@if ($displayName)
|
||||
<h4>{{ $displayName }}</h4>
|
||||
@elseif ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))
|
||||
<h4>{{ $container }}</h4>
|
||||
@else
|
||||
<h4>{{ str($container)->beforeLast('-')->headline() }}</h4>
|
||||
@endif
|
||||
@if ($pull_request)
|
||||
<div>({{ $pull_request }})</div>
|
||||
@endif
|
||||
@if ($streamLogs)
|
||||
<x-loading wire:poll.2000ms='getLogs(true)' />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:gap-2 sm:items-center">
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
|
||||
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<div :class="fullscreen ? 'fullscreen' : 'relative w-full py-4 mx-auto'">
|
||||
<div class="flex overflow-y-auto overflow-x-hidden flex-col-reverse px-4 py-2 w-full min-w-0 bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300 border-neutral-200"
|
||||
:class="fullscreen ? '' : 'max-h-96 border border-solid rounded-sm'">
|
||||
<div :class="fullscreen ? 'fixed top-4 right-4' : 'absolute top-6 right-0'">
|
||||
<div class="flex justify-end gap-4" :class="fullscreen ? 'fixed' : ''"
|
||||
style="transform: translateX(-100%)">
|
||||
{{-- <button title="Go Top" x-show="fullscreen" x-on:click="goTop">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-10l-4-4M8 9l4-4" />
|
||||
@endif
|
||||
<div x-show="expanded" {{ $collapsible ? 'x-collapse' : '' }}
|
||||
:class="fullscreen ? 'fullscreen flex flex-col' : 'relative w-full {{ $collapsible ? 'py-4' : '' }} mx-auto'">
|
||||
<div class="flex flex-col bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300 border-neutral-200"
|
||||
:class="fullscreen ? 'h-full' : 'border border-solid rounded-sm'">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<form wire:submit="getLogs(true)" class="relative flex items-center">
|
||||
<span
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 pointer-events-none">Lines:</span>
|
||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="1"
|
||||
title="Number of Lines" {{ $streamLogs ? 'readonly' : '' }}
|
||||
class="input input-sm w-32 pl-11 text-center dark:bg-coolgray-300" />
|
||||
</form>
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input type="text" x-model="searchQuery" placeholder="Find in logs"
|
||||
class="input input-sm w-48 pl-8 pr-8 dark:bg-coolgray-300" />
|
||||
<button x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button wire:click="getLogs(true)" title="Refresh Logs" {{ $streamLogs ? 'disabled' : '' }}
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'dark:text-warning' : ''"
|
||||
x-on:click="toggleScroll">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</button> --}}
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
</button>
|
||||
<button wire:click="toggleTimestamps" title="Toggle Timestamps"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $showTimeStamps ? '!text-warning' : '' }}">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Toggle Log Colors" x-on:click="toggleColorLogs"
|
||||
:class="colorLogs ? '!text-warning' : ''"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:click="toggleStreamLogs"
|
||||
title="{{ $streamLogs ? 'Stop Streaming' : 'Stream Logs' }}"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $streamLogs ? '!text-warning' : '' }}">
|
||||
@if ($streamLogs)
|
||||
{{-- Pause icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
||||
</svg>
|
||||
@else
|
||||
{{-- Play icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M8 5v14l11-7L8 5z" />
|
||||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? '!text-warning' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
|
|
@ -91,77 +294,75 @@
|
|||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100"
|
||||
viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if ($outputs)
|
||||
<div id="logs" class="font-mono text-sm">
|
||||
@foreach (explode("\n", trim($outputs)) as $line)
|
||||
@if (!empty(trim($line)))
|
||||
<div id="logsContainer" @scroll="handleScroll"
|
||||
class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[40rem]'">
|
||||
@if ($outputs)
|
||||
<div id="logs" class="font-mono max-w-full cursor-default">
|
||||
<div x-show="searchQuery.trim() && getMatchCount() === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
</div>
|
||||
@foreach (explode("\n", $outputs) as $line)
|
||||
@php
|
||||
$lowerLine = strtolower($line);
|
||||
$isError =
|
||||
str_contains($lowerLine, 'error') ||
|
||||
str_contains($lowerLine, 'err') ||
|
||||
str_contains($lowerLine, 'failed') ||
|
||||
str_contains($lowerLine, 'exception');
|
||||
$isWarning =
|
||||
str_contains($lowerLine, 'warn') ||
|
||||
str_contains($lowerLine, 'warning') ||
|
||||
str_contains($lowerLine, 'wrn');
|
||||
$isDebug =
|
||||
str_contains($lowerLine, 'debug') ||
|
||||
str_contains($lowerLine, 'dbg') ||
|
||||
str_contains($lowerLine, 'trace');
|
||||
$barColor = $isError
|
||||
? 'bg-red-500 dark:bg-red-400'
|
||||
: ($isWarning
|
||||
? 'bg-warning-500 dark:bg-warning-400'
|
||||
: ($isDebug
|
||||
? 'bg-purple-500 dark:bg-purple-400'
|
||||
: 'bg-blue-500 dark:bg-blue-400'));
|
||||
$bgColor = $isError
|
||||
? 'bg-red-50/50 dark:bg-red-900/20 hover:bg-red-100/50 dark:hover:bg-red-800/30'
|
||||
: ($isWarning
|
||||
? 'bg-warning-50/50 dark:bg-warning-900/20 hover:bg-warning-100/50 dark:hover:bg-warning-800/30'
|
||||
: ($isDebug
|
||||
? 'bg-purple-50/50 dark:bg-purple-900/20 hover:bg-purple-100/50 dark:hover:bg-purple-800/30'
|
||||
: 'bg-blue-50/50 dark:bg-blue-900/20 hover:bg-blue-100/50 dark:hover:bg-blue-800/30'));
|
||||
// Skip empty lines
|
||||
if (trim($line) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z)
|
||||
$timestamp = '';
|
||||
$logContent = $line;
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z?\s*(.*)$/', $line, $matches)) {
|
||||
$year = $matches[1];
|
||||
$month = $matches[2];
|
||||
$day = $matches[3];
|
||||
$time = $matches[4];
|
||||
$microseconds = isset($matches[5]) ? substr($matches[5], 0, 6) : '000000';
|
||||
$logContent = $matches[6];
|
||||
|
||||
// Convert month number to abbreviated name
|
||||
$monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
$monthName = $monthNames[(int)$month - 1] ?? $month;
|
||||
|
||||
// Format: 2025-Dec-04 09:44:58.198879
|
||||
$timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}";
|
||||
}
|
||||
|
||||
// Check for timestamp at the beginning (ISO 8601 format)
|
||||
$timestampPattern = '/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z?)\s+/';
|
||||
$hasTimestamp = preg_match($timestampPattern, $line, $matches);
|
||||
$timestamp = $hasTimestamp ? $matches[1] : null;
|
||||
$logContent = $hasTimestamp ? preg_replace($timestampPattern, '', $line) : $line;
|
||||
@endphp
|
||||
<div class="flex items-start gap-2 py-1 px-2 rounded-sm">
|
||||
<div class="w-1 {{ $barColor }} rounded-full flex-shrink-0 self-stretch"></div>
|
||||
<div class="flex-1 {{ $bgColor }} py-1 px-2 -mx-2 rounded-sm">
|
||||
@if ($hasTimestamp)
|
||||
<span
|
||||
class="text-xs text-gray-500 dark:text-gray-400 font-mono mr-2">{{ $timestamp }}</span>
|
||||
<span class="whitespace-pre-wrap break-all">{{ $logContent }}</span>
|
||||
@else
|
||||
<span class="whitespace-pre-wrap break-all">{{ $line }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div data-log-line data-log-content="{{ $line }}"
|
||||
x-bind:class="{
|
||||
'hidden': !matchesSearch($el.dataset.logContent),
|
||||
'bg-red-500/10 dark:bg-red-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'error',
|
||||
'bg-yellow-500/10 dark:bg-yellow-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'warning',
|
||||
'bg-purple-500/10 dark:bg-purple-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'debug',
|
||||
'bg-blue-500/10 dark:bg-blue-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'info',
|
||||
}"
|
||||
class="flex gap-2">
|
||||
@if ($timestamp && $showTimeStamps)
|
||||
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
|
||||
@endif
|
||||
<span data-line-text="{{ $logContent }}"
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
class="whitespace-pre-wrap break-all"></span>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div id="logs" class="font-mono text-sm py-4 px-2 text-gray-500 dark:text-gray-400">
|
||||
Refresh to get the logs...
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<pre id="logs"
|
||||
class="font-mono whitespace-pre-wrap break-all max-w-full">Refresh to get the logs...</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -17,13 +17,17 @@
|
|||
<div x-init="$wire.loadAllContainers()" wire:loading.remove wire:target="loadAllContainers">
|
||||
@forelse ($servers as $server)
|
||||
<div class="py-2">
|
||||
<h2>Server: {{ $server->name }}</h2>
|
||||
<h4>Server: {{ $server->name }}</h4>
|
||||
@if ($server->isFunctional())
|
||||
@if (isset($serverContainers[$server->id]) && count($serverContainers[$server->id]) > 0)
|
||||
@php
|
||||
$totalContainers = collect($serverContainers)->flatten(1)->count();
|
||||
@endphp
|
||||
@foreach ($serverContainers[$server->id] as $container)
|
||||
<livewire:project.shared.get-logs
|
||||
wire:key="{{ data_get($container, 'ID', uniqid()) }}" :server="$server"
|
||||
:resource="$resource" :container="data_get($container, 'Names')" />
|
||||
:resource="$resource" :container="data_get($container, 'Names')"
|
||||
:expandByDefault="$totalContainers === 1" />
|
||||
@endforeach
|
||||
@else
|
||||
<div class="pt-2">No containers are running on server: {{ $server->name }}</div>
|
||||
|
|
@ -53,7 +57,8 @@
|
|||
@forelse ($containers as $container)
|
||||
@if (data_get($servers, '0'))
|
||||
<livewire:project.shared.get-logs wire:key='{{ $container }}' :server="data_get($servers, '0')"
|
||||
:resource="$resource" :container="$container" />
|
||||
:resource="$resource" :container="$container"
|
||||
:expandByDefault="count($containers) === 1" />
|
||||
@else
|
||||
<div>No functional server found for the database.</div>
|
||||
@endif
|
||||
|
|
@ -77,7 +82,8 @@
|
|||
@forelse ($containers as $container)
|
||||
@if (data_get($servers, '0'))
|
||||
<livewire:project.shared.get-logs wire:key='{{ $container }}' :server="data_get($servers, '0')"
|
||||
:resource="$resource" :container="$container" />
|
||||
:resource="$resource" :container="$container"
|
||||
:expandByDefault="count($containers) === 1" />
|
||||
@else
|
||||
<div>No functional server found for the service.</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@
|
|||
<li>Networks not attached to running containers will be permanently deleted (networks used by stopped containers are affected).</li>
|
||||
<li>Containers may lose connectivity if required networks are removed.</li>
|
||||
</ul>" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
id="disableApplicationImageRetention"
|
||||
label="Disable Application Image Retention"
|
||||
helper="When enabled, Docker cleanup will delete all old application images regardless of per-application retention settings. Only the currently running image will be kept.<br><br><strong>Warning: This disables rollback capabilities for all applications on this server.</strong>" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
<x-slide-over @startproxy.window="slideOverOpen = true" fullScreen closeWithX>
|
||||
<x-slot:title>Proxy Startup Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
@if ($server->id === 0)
|
||||
<div class="mb-4 p-3 text-sm bg-warning/10 border border-warning/30 rounded-lg text-warning">
|
||||
<span class="font-semibold">Note:</span> This is the localhost server where Coolify runs.
|
||||
During proxy restart, the connection may be temporarily lost.
|
||||
If logs stop updating, please refresh the browser after a few minutes.
|
||||
</div>
|
||||
@endif
|
||||
<livewire:activity-monitor header="Logs" fullHeight />
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
|
|
@ -174,6 +181,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
|
|||
}
|
||||
});
|
||||
$wire.$on('restartEvent', () => {
|
||||
if ($wire.restartInitiated) return;
|
||||
window.dispatchEvent(new CustomEvent('startproxy'))
|
||||
$wire.$call('restart');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<h2 class="pb-4">Logs</h2>
|
||||
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" displayName="Coolify Proxy" />
|
||||
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" displayName="Coolify Proxy" :collapsible="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -337,7 +337,8 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" lazy />
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
|
|
@ -353,7 +354,8 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" lazy />
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -18,28 +18,28 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
|||
<div class="flex flex-col gap-1">
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled"
|
||||
helper="If enabled, users can register themselves. If disabled, only administrators can create new users."
|
||||
helper="Allow users to self-register. If disabled, only administrators can create accounts."
|
||||
label="Registration Allowed" />
|
||||
</div>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="do_not_track"
|
||||
helper="If enabled, Coolify will not track any data. This is useful if you are concerned about privacy."
|
||||
helper="Opt out of reporting this instance to coolify.io's installation count. No other data is collected."
|
||||
label="Do Not Track" />
|
||||
</div>
|
||||
<h4 class="pt-4">DNS Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_dns_validation_enabled"
|
||||
helper="If you set a custom domain, Coolify will validate the domain in your DNS provider."
|
||||
helper="Verify that custom domains are correctly configured in DNS before deployment. Prevents deployment failures from DNS misconfigurations."
|
||||
label="DNS Validation" />
|
||||
</div>
|
||||
|
||||
<x-forms.input id="custom_dns_servers" label="Custom DNS Servers"
|
||||
helper="DNS servers to validate domains against. A comma separated list of DNS servers."
|
||||
helper="Custom DNS servers for domain validation. Comma-separated list (e.g., 1.1.1.1,8.8.8.8). Leave empty to use system defaults."
|
||||
placeholder="1.1.1.1,8.8.8.8" />
|
||||
<h4 class="pt-4">API Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_api_enabled" label="API Access"
|
||||
helper="If enabled, the API will be enabled. If disabled, the API will be disabled." />
|
||||
helper="If enabled, authenticated requests to Coolify's REST API will be allowed. Configure API tokens in Security > API Tokens." />
|
||||
</div>
|
||||
<x-forms.input id="allowed_ips" label="Allowed IPs for API Access"
|
||||
helper="Allowed IP addresses or subnets for API access.<br>Supports single IPs (192.168.1.100) and CIDR notation (192.168.1.0/24).<br>Use comma to separate multiple entries.<br>Use 0.0.0.0 or leave empty to allow from anywhere."
|
||||
|
|
@ -53,7 +53,7 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
|||
<h4 class="pt-4">Confirmation Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id=" is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
helper="When enabled, sponsorship popups will be shown monthly to users. When disabled, the sponsorship popup will be permanently hidden for all users." />
|
||||
helper="Show monthly sponsorship reminders to support Coolify development. Disable to hide these messages permanently." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
|
|
|
|||
30
templates/compose/fizzy.yaml
Normal file
30
templates/compose/fizzy.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# documentation: https://github.com/basecamp/fizzy
|
||||
# slogan: Kanban tracking tool for issues and ideas by 37signals
|
||||
# category: productivity
|
||||
# tags: kanban, project management, issues, rails, ruby, basecamp, 37signals
|
||||
# logo: svgs/fizzy.png
|
||||
# port: 80
|
||||
|
||||
services:
|
||||
fizzy:
|
||||
image: ghcr.io/basecamp/fizzy:main
|
||||
environment:
|
||||
- SERVICE_FQDN_FIZZY_80
|
||||
- SECRET_KEY_BASE=$SERVICE_PASSWORD_FIZZY
|
||||
- RAILS_MASTER_KEY=$SERVICE_PASSWORD_64_MASTERKEY
|
||||
- RAILS_ENV=production
|
||||
- RAILS_LOG_TO_STDOUT=true
|
||||
- RAILS_SERVE_STATIC_FILES=true
|
||||
- DATABASE_ADAPTER=sqlite
|
||||
- SOLID_QUEUE_CONNECTS_TO=false
|
||||
- VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
|
||||
- VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
|
||||
volumes:
|
||||
- fizzy-data:/rails/db
|
||||
- fizzy-storage:/rails/storage
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:80/up"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
59
templates/compose/garage.yaml
Normal file
59
templates/compose/garage.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# ignore: true
|
||||
# documentation: https://garagehq.deuxfleurs.fr/documentation/
|
||||
# slogan: Garage is an S3-compatible distributed object storage service designed for self-hosting.
|
||||
# category: storage
|
||||
# tags: object, storage, server, s3, api, distributed
|
||||
# logo: svgs/garage.svg
|
||||
# port: 3900
|
||||
|
||||
services:
|
||||
garage:
|
||||
image: dxflrs/garage:v2.1.0
|
||||
environment:
|
||||
- GARAGE_S3_API_URL=$GARAGE_S3_API_URL
|
||||
- GARAGE_WEB_URL=$GARAGE_WEB_URL
|
||||
- GARAGE_ADMIN_URL=$GARAGE_ADMIN_URL
|
||||
- GARAGE_RPC_SECRET=${SERVICE_HEX_32_RPCSECRET}
|
||||
- GARAGE_ADMIN_TOKEN=$SERVICE_PASSWORD_GARAGE
|
||||
- GARAGE_METRICS_TOKEN=$SERVICE_PASSWORD_GARAGEMETRICS
|
||||
- GARAGE_ALLOW_WORLD_READABLE_SECRETS=true
|
||||
- RUST_LOG=${RUST_LOG:-garage=info}
|
||||
volumes:
|
||||
- garage-meta:/var/lib/garage/meta
|
||||
- garage-data:/var/lib/garage/data
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
content: |
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
db_engine = "lmdb"
|
||||
|
||||
replication_factor = 1
|
||||
consistency_mode = "consistent"
|
||||
|
||||
compression_level = 1
|
||||
block_size = "1M"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_secret_file = "env:GARAGE_RPC_SECRET"
|
||||
bootstrap_peers = []
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
root_domain = ".s3.garage.localhost"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage.localhost"
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
admin_token_file = "env:GARAGE_ADMIN_TOKEN"
|
||||
metrics_token_file = "env:GARAGE_METRICS_TOKEN"
|
||||
healthcheck:
|
||||
test: ["CMD", "/garage", "stats", "-a"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
35
templates/compose/rustfs.yaml
Normal file
35
templates/compose/rustfs.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ignore: true
|
||||
# documentation: https://docs.rustfs.com/installation/docker/
|
||||
# slogan: RustFS is a high-performance distributed storage system built with Rust, compatible with Amazon S3 APIs.
|
||||
# category: storage
|
||||
# tags: object, storage, server, s3, api, rust
|
||||
# logo: svgs/rustfs.png
|
||||
|
||||
services:
|
||||
rustfs:
|
||||
image: rustfs/rustfs:latest
|
||||
command: /data
|
||||
environment:
|
||||
- RUSTFS_SERVER_URL=$RUSTFS_SERVER_URL
|
||||
- RUSTFS_BROWSER_REDIRECT_URL=$RUSTFS_BROWSER_REDIRECT_URL
|
||||
- RUSTFS_ADDRESS=${RUSTFS_ADDRESS:-0.0.0.0:9000}
|
||||
- RUSTFS_CONSOLE_ADDRESS=${RUSTFS_CONSOLE_ADDRESS:-0.0.0.0:9001}
|
||||
- RUSTFS_CORS_ALLOWED_ORIGINS=${RUSTFS_CORS_ALLOWED_ORIGINS:-*}
|
||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=${RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS:-*}
|
||||
- RUSTFS_ACCESS_KEY=$SERVICE_USER_RUSTFS
|
||||
- RUSTFS_SECRET_KEY=$SERVICE_PASSWORD_RUSTFS
|
||||
- RUSTFS_CONSOLE_ENABLE=${RUSTFS_CONSOLE_ENABLE:-true}
|
||||
- RUSTFS_SERVER_DOMAINS=${RUSTFS_SERVER_DOMAINS}
|
||||
- RUSTFS_EXTERNAL_ADDRESS=${RUSTFS_EXTERNAL_ADDRESS}
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"sh", "-c",
|
||||
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health"
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue