diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 0349ead89..ba4c2311a 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -25,17 +25,25 @@ class CleanupDocker "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", ]; - $serverSettings = $server->settings; - if ($serverSettings->delete_unused_volumes) { + if ($server->settings->delete_unused_volumes) { $commands[] = 'docker volume prune -af'; } - if ($serverSettings->delete_unused_networks) { + if ($server->settings->delete_unused_networks) { $commands[] = 'docker network prune -f'; } + $cleanupLog = []; foreach ($commands as $command) { - instant_remote_process([$command], $server, false); + $commandOutput = instant_remote_process([$command], $server, false); + if ($commandOutput !== null) { + $cleanupLog[] = [ + 'command' => $command, + 'output' => $commandOutput, + ]; + } } + + return $cleanupLog; } } diff --git a/app/Events/DockerCleanupDone.php b/app/Events/DockerCleanupDone.php new file mode 100644 index 000000000..2bd7fee19 --- /dev/null +++ b/app/Events/DockerCleanupDone.php @@ -0,0 +1,24 @@ +execution->server->team->id), + ]; + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 98bd314e9..37c73d10f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -18,7 +18,7 @@ use App\Models\SwarmDocker; use App\Notifications\Application\DeploymentFailed; use App\Notifications\Application\DeploymentSuccess; use App\Traits\ExecuteRemoteCommand; -use Exception; +use Carbon\Carbon; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -317,7 +317,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue throw $e; } finally { $this->application_deployment_queue->update([ - 'finished_at' => now(), + 'finished_at' => Carbon::now()->toImmutable(), ]); if ($this->use_build_server) { @@ -1501,7 +1501,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ] ); if ($this->saved_outputs->get('commit_message')) { - $commit_message = str($this->saved_outputs->get('commit_message'))->limit(47); + $commit_message = str($this->saved_outputs->get('commit_message')); $this->application_deployment_queue->commit_message = $commit_message->value(); ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update( ['commit_message' => $commit_message->value()] diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 577c1f11a..0861c6bcc 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -331,6 +331,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->team) { BackupCreated::dispatch($this->team->id); } + if ($this->backup_log) { + $this->backup_log->update([ + 'finished_at' => Carbon::now()->toImmutable(), + ]); + } } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 103c137b9..05a4aa8de 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -3,9 +3,12 @@ namespace App\Jobs; use App\Actions\Server\CleanupDocker; +use App\Events\DockerCleanupDone; +use App\Models\DockerCleanupExecution; use App\Models\Server; use App\Notifications\Server\DockerCleanupFailed; use App\Notifications\Server\DockerCleanupSuccess; +use Carbon\Carbon; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -24,6 +27,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?string $usageBefore = null; + public ?DockerCleanupExecution $execution_log = null; + public function middleware(): array { return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; @@ -38,37 +43,89 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue return; } + $this->execution_log = DockerCleanupExecution::create([ + 'server_id' => $this->server->id, + ]); + $this->usageBefore = $this->server->getDiskUsage(); if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { - CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run(server: $this->server); $usageAfter = $this->server->getDiskUsage(); - $this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); + $message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'; + + $this->execution_log->update([ + 'status' => 'success', + 'message' => $message, + 'cleanup_log' => $cleanup_log, + ]); + + $this->server->team?->notify(new DockerCleanupSuccess($this->server, $message)); + event(new DockerCleanupDone($this->execution_log)); return; } if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { - CleanupDocker::run(server: $this->server); - $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.')); + $cleanup_log = CleanupDocker::run(server: $this->server); + $message = 'Docker cleanup job executed successfully, but no disk usage could be determined.'; + + $this->execution_log->update([ + 'status' => 'success', + 'message' => $message, + 'cleanup_log' => $cleanup_log, + ]); + + $this->server->team?->notify(new DockerCleanupSuccess($this->server, $message)); + event(new DockerCleanupDone($this->execution_log)); } if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) { - CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run(server: $this->server); $usageAfter = $this->server->getDiskUsage(); $diskSaved = $this->usageBefore - $usageAfter; if ($diskSaved > 0) { - $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); + $message = 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'; } else { - $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); + $message = 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'; } + + $this->execution_log->update([ + 'status' => 'success', + 'message' => $message, + 'cleanup_log' => $cleanup_log, + ]); + + $this->server->team?->notify(new DockerCleanupSuccess($this->server, $message)); + event(new DockerCleanupDone($this->execution_log)); } else { - $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name)); + $message = 'No cleanup needed for '.$this->server->name; + + $this->execution_log->update([ + 'status' => 'success', + 'message' => $message, + ]); + + $this->server->team?->notify(new DockerCleanupSuccess($this->server, $message)); + event(new DockerCleanupDone($this->execution_log)); } } catch (\Throwable $e) { + if ($this->execution_log) { + $this->execution_log->update([ + 'status' => 'failed', + 'message' => $e->getMessage(), + ]); + event(new DockerCleanupDone($this->execution_log)); + } $this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage())); throw $e; + } finally { + if ($this->execution_log) { + $this->execution_log->update([ + 'finished_at' => Carbon::now()->toImmutable(), + ]); + } } } } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 90a10f3e9..6c0c017e7 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -11,6 +11,7 @@ use App\Models\Service; use App\Models\Team; use App\Notifications\ScheduledTask\TaskFailed; use App\Notifications\ScheduledTask\TaskSuccess; +use Carbon\Carbon; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -131,6 +132,11 @@ class ScheduledTaskJob implements ShouldQueue throw $e; } finally { ScheduledTaskDone::dispatch($this->team->id); + if ($this->task_log) { + $this->task_log->update([ + 'finished_at' => Carbon::now()->toImmutable(), + ]); + } } } } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 7eef1a539..3fc721fda 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -117,29 +117,6 @@ class BackupExecutions extends Component return null; } - public function getServerTimezone() - { - $server = $this->server(); - if (! $server) { - return 'UTC'; - } - - return $server->settings->server_timezone; - } - - public function formatDateInServerTimezone($date) - { - $serverTimezone = $this->getServerTimezone(); - $dateObj = new \DateTime($date); - try { - $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); - } catch (\Exception) { - $dateObj->setTimezone(new \DateTimeZone('UTC')); - } - - return $dateObj->format('Y-m-d H:i:s T'); - } - public function render() { return view('livewire.project.database.backup-executions', [ diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 74eac7132..6f62a5b5b 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -141,17 +141,4 @@ class Executions extends Component return $lines->count() > ($this->currentPage * $this->logsPerPage); } - - public function formatDateInServerTimezone($date) - { - $serverTimezone = $this->serverTimezone; - $dateObj = new \DateTime($date); - try { - $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); - } catch (\Exception) { - $dateObj->setTimezone(new \DateTimeZone('UTC')); - } - - return $dateObj->format('Y-m-d H:i:s T'); - } } diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index 577730f24..b269c916f 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -2,7 +2,6 @@ namespace App\Livewire\Server; -use App\Jobs\DockerCleanupJob; use App\Models\Server; use Livewire\Attributes\Validate; use Livewire\Component; @@ -19,21 +18,6 @@ class Advanced extends Component #[Validate(['integer', 'min:1', 'max:99'])] public int $serverDiskUsageNotificationThreshold = 50; - #[Validate(['string', 'required'])] - public string $dockerCleanupFrequency = '*/10 * * * *'; - - #[Validate(['integer', 'min:1', 'max:99'])] - public int $dockerCleanupThreshold = 10; - - #[Validate('boolean')] - public bool $forceDockerCleanup = false; - - #[Validate('boolean')] - public bool $deleteUnusedVolumes = false; - - #[Validate('boolean')] - public bool $deleteUnusedNetworks = false; - #[Validate(['integer', 'min:1'])] public int $concurrentBuilds = 1; @@ -47,7 +31,7 @@ class Advanced extends Component $this->parameters = get_route_parameters(); $this->syncData(); } catch (\Throwable) { - return redirect()->route('server.show'); + return redirect()->route('server.index'); } } @@ -57,23 +41,13 @@ class Advanced extends Component $this->validate(); $this->server->settings->concurrent_builds = $this->concurrentBuilds; $this->server->settings->dynamic_timeout = $this->dynamicTimeout; - $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; - $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; - $this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold; $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold; - $this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes; - $this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks; $this->server->settings->server_disk_usage_check_frequency = $this->serverDiskUsageCheckFrequency; $this->server->settings->save(); } else { $this->concurrentBuilds = $this->server->settings->concurrent_builds; $this->dynamicTimeout = $this->server->settings->dynamic_timeout; - $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup; - $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency; - $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold; $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold; - $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes; - $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks; $this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency; } } @@ -88,23 +62,9 @@ class Advanced extends Component } } - public function manualCleanup() - { - try { - DockerCleanupJob::dispatch($this->server, true); - $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - public function submit() { try { - if (! validate_cron_expression($this->dockerCleanupFrequency)) { - $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency'); - throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.'); - } if (! validate_cron_expression($this->serverDiskUsageCheckFrequency)) { $this->serverDiskUsageCheckFrequency = $this->server->settings->getOriginal('server_disk_usage_check_frequency'); throw new \Exception('Invalid Cron / Human expression for Disk Usage Check Frequency.'); diff --git a/app/Livewire/Server/DockerCleanup.php b/app/Livewire/Server/DockerCleanup.php new file mode 100644 index 000000000..d3378d63f --- /dev/null +++ b/app/Livewire/Server/DockerCleanup.php @@ -0,0 +1,99 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable) { + return redirect()->route('server.index'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; + $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; + $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->save(); + } else { + $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup; + $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency; + $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold; + $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes; + $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks; + } + } + + public function instantSave() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCleanup() + { + try { + DockerCleanupJob::dispatch($this->server, true); + $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + if (! validate_cron_expression($this->dockerCleanupFrequency)) { + $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency'); + throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.'); + } + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.docker-cleanup'); + } +} diff --git a/app/Livewire/Server/DockerCleanupExecutions.php b/app/Livewire/Server/DockerCleanupExecutions.php new file mode 100644 index 000000000..56d613064 --- /dev/null +++ b/app/Livewire/Server/DockerCleanupExecutions.php @@ -0,0 +1,132 @@ +user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DockerCleanupDone" => 'refreshExecutions', + ]; + } + + public function mount(Server $server) + { + $this->server = $server; + $this->refreshExecutions(); + } + + public function refreshExecutions(): void + { + $this->executions = $this->server->dockerCleanupExecutions() + ->orderBy('created_at', 'desc') + ->take(20) + ->get(); + + if ($this->selectedKey) { + $this->selectedExecution = DockerCleanupExecution::find($this->selectedKey); + if ($this->selectedExecution && $this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } + } + + public function selectExecution($key): void + { + if ($key == $this->selectedKey) { + $this->selectedKey = null; + $this->selectedExecution = null; + $this->currentPage = 1; + $this->isPollingActive = false; + + return; + } + $this->selectedKey = $key; + $this->selectedExecution = DockerCleanupExecution::find($key); + $this->currentPage = 1; + + if ($this->selectedExecution && $this->selectedExecution->status === 'running') { + $this->isPollingActive = true; + } + } + + public function polling() + { + if ($this->selectedExecution && $this->isPollingActive) { + $this->selectedExecution->refresh(); + if ($this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } + $this->refreshExecutions(); + } + + public function loadMoreLogs() + { + $this->currentPage++; + } + + public function getLogLinesProperty() + { + if (! $this->selectedExecution) { + return collect(); + } + + if (! $this->selectedExecution->message) { + return collect(['Waiting for execution output...']); + } + + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->take($this->currentPage * $this->logsPerPage); + } + + public function downloadLogs(int $executionId) + { + $execution = $this->executions->firstWhere('id', $executionId); + if (! $execution) { + return; + } + + return response()->streamDownload(function () use ($execution) { + echo $execution->message; + }, "docker-cleanup-{$execution->uuid}.log"); + } + + public function hasMoreLogs() + { + if (! $this->selectedExecution || ! $this->selectedExecution->message) { + return false; + } + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->count() > ($this->currentPage * $this->logsPerPage); + } + + public function render() + { + return view('livewire.server.docker-cleanup-executions'); + } +} diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 95c78d725..fd8f1cba2 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -81,7 +81,7 @@ class ApplicationDeploymentQueue extends Model return null; } - return str($this->commit_message)->trim()->limit(50)->value(); + return str($this->commit_message)->value(); } public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false) diff --git a/app/Models/DockerCleanupExecution.php b/app/Models/DockerCleanupExecution.php new file mode 100644 index 000000000..405037e30 --- /dev/null +++ b/app/Models/DockerCleanupExecution.php @@ -0,0 +1,15 @@ +belongsTo(Server::class); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 2867f95cb..f3edd82fb 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -199,6 +199,11 @@ class Server extends BaseModel return $this->hasOne(ServerSetting::class); } + public function dockerCleanupExecutions() + { + return $this->hasMany(DockerCleanupExecution::class); + } + public function proxySet() { return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; diff --git a/bootstrap/helpers/timezone.php b/bootstrap/helpers/timezone.php new file mode 100644 index 000000000..96d9ba6cf --- /dev/null +++ b/bootstrap/helpers/timezone.php @@ -0,0 +1,42 @@ +setTimezone(new \DateTimeZone($serverTimezone)); + } catch (\Exception) { + $dateObj->setTimezone(new \DateTimeZone('UTC')); + } + + return $dateObj->format('Y-m-d H:i:s T'); +} + +function calculateDuration($startDate, $endDate = null) +{ + if (! $endDate) { + return null; + } + + $start = new \DateTime($startDate); + $end = new \DateTime($endDate); + $interval = $start->diff($end); + + if ($interval->days > 0) { + return $interval->format('%dd %Hh %Im %Ss'); + } elseif ($interval->h > 0) { + return $interval->format('%Hh %Im %Ss'); + } else { + return $interval->format('%Im %Ss'); + } +} diff --git a/database/migrations/2025_01_15_130416_create_docker_cleanup_executions_table.php b/database/migrations/2025_01_15_130416_create_docker_cleanup_executions_table.php new file mode 100644 index 000000000..c96213125 --- /dev/null +++ b/database/migrations/2025_01_15_130416_create_docker_cleanup_executions_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('uuid')->unique(); + $table->enum('status', ['success', 'failed', 'running'])->default('running'); + $table->text('message')->nullable(); + $table->json('cleanup_log')->nullable(); + $table->foreignId('server_id'); + $table->timestamps(); + $table->timestamp('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('docker_cleanup_executions'); + } +}; diff --git a/database/migrations/2025_01_16_130238_add_deployment_queue_finished_at.php b/database/migrations/2025_01_16_110406_change_commit_message_to_text_in_application_deployment_queues.php similarity index 57% rename from database/migrations/2025_01_16_130238_add_deployment_queue_finished_at.php rename to database/migrations/2025_01_16_110406_change_commit_message_to_text_in_application_deployment_queues.php index 34b652ee0..6c64bf38d 100644 --- a/database/migrations/2025_01_16_130238_add_deployment_queue_finished_at.php +++ b/database/migrations/2025_01_16_110406_change_commit_message_to_text_in_application_deployment_queues.php @@ -6,17 +6,23 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - public function up() + /** + * Run the migrations. + */ + public function up(): void { Schema::table('application_deployment_queues', function (Blueprint $table) { - $table->timestamp('finished_at')->nullable(); + $table->text('commit_message')->nullable()->change(); }); } - public function down() + /** + * Reverse the migrations. + */ + public function down(): void { Schema::table('application_deployment_queues', function (Blueprint $table) { - $table->dropColumn('finished_at'); + $table->string('commit_message', 50)->nullable()->change(); }); } }; diff --git a/database/migrations/2025_01_16_130238_add_finished_at_to_executions_tables.php b/database/migrations/2025_01_16_130238_add_finished_at_to_executions_tables.php new file mode 100644 index 000000000..0b78c8998 --- /dev/null +++ b/database/migrations/2025_01_16_130238_add_finished_at_to_executions_tables.php @@ -0,0 +1,38 @@ +timestamp('finished_at')->nullable(); + }); + Schema::table('scheduled_database_backup_executions', function (Blueprint $table) { + $table->timestamp('finished_at')->nullable(); + }); + + Schema::table('scheduled_task_executions', function (Blueprint $table) { + $table->timestamp('finished_at')->nullable(); + }); + + } + + public function down() + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('finished_at'); + }); + + Schema::table('scheduled_database_backup_executions', function (Blueprint $table) { + $table->dropColumn('finished_at'); + }); + + Schema::table('scheduled_task_executions', function (Blueprint $table) { + $table->dropColumn('finished_at'); + }); + } +}; diff --git a/public/js/dayjs-plugin-relativeTime.js b/public/js/dayjs-plugin-relativeTime.js deleted file mode 100644 index 898eee6d0..000000000 --- a/public/js/dayjs-plugin-relativeTime.js +++ /dev/null @@ -1 +0,0 @@ -!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).dayjs_plugin_relativeTime=e()}(this,(function(){"use strict";return function(r,e,t){r=r||{};var n=e.prototype,o={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:"s",r:44,d:"second"},{l:"m",r:89},{l:"mm",r:44,d:"minute"},{l:"h",r:89},{l:"hh",r:21,d:"hour"},{l:"d",r:35},{l:"dd",r:25,d:"day"},{l:"M",r:45},{l:"MM",r:10,d:"month"},{l:"y",r:17},{l:"yy",d:"year"}],m=h.length,c=0;c0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(""+p)),a="string"==typeof v?v.replace("%d",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return"function"==typeof M?M(a):M.replace("%s",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}})); \ No newline at end of file diff --git a/public/js/dayjs-plugin-utc.js b/public/js/dayjs-plugin-utc.js deleted file mode 100644 index 608771e95..000000000 --- a/public/js/dayjs-plugin-utc.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_utc=i()}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var o=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),o.call(this,t)};var r=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else r.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if("string"==typeof s&&(s=function(t){void 0===t&&(t="");var s=t.match(i);if(!s)return null;var f=(""+s[0]).match(e)||["-",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:"+"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s,o=this;if(f)return o.$offset=u,o.$u=0===s,o;if(0!==s){var r=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();(o=this.local().add(u+r,t)).$offset=u,o.$x.$localOffset=r}else o=this.utc();return o};var h=u.format;u.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return"s"===t&&this.$offset?n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}})); diff --git a/public/js/dayjs.min.js b/public/js/dayjs.min.js deleted file mode 100644 index 76f599020..000000000 --- a/public/js/dayjs.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",c="month",f="quarter",h="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t) General - @if ($server->isFunctional()) - Advanced - - @endif Private Key @@ -15,9 +10,15 @@ Tunnels @endif @if ($server->isFunctional()) + Docker Cleanup + Destinations + Advanced + Log Drains diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index aae357401..db7aad83e 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -40,9 +40,6 @@ - - - @endauth @section('body') diff --git a/resources/views/livewire/project/application/deployment/index.blade.php b/resources/views/livewire/project/application/deployment/index.blade.php index 6914e933e..ec5cf0f98 100644 --- a/resources/views/livewire/project/application/deployment/index.blade.php +++ b/resources/views/livewire/project/application/deployment/index.blade.php @@ -1,25 +1,22 @@
- - {{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify - + {{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify

Deployments

-
+

Deployments ({{ $deployments_count }})

- @if ($deployments_count > 0 && $deployments_count > $default_take) - - - - - - + @if ($deployments_count > 0) + + + + + + + + + + @endif
@if ($deployments_count > 0) @@ -30,139 +27,132 @@ @endif @forelse ($deployments as $deployment)
- data_get($deployment, 'status') === 'in_progress' || - data_get($deployment, 'status') === 'cancelled-by-user', - 'border-error border-dashed ' => - data_get($deployment, 'status') === 'failed', + 'p-2 border-l-2 bg-white dark:bg-coolgray-100', + 'border-blue-500/50 border-dashed' => data_get($deployment, 'status') === 'in_progress', + 'border-purple-500/50 border-dashed' => data_get($deployment, 'status') === 'queued', + 'border-white border-dashed' => data_get($deployment, 'status') === 'cancelled-by-user', + 'border-error' => data_get($deployment, 'status') === 'failed', 'border-success' => data_get($deployment, 'status') === 'finished', - ]) wire:navigate - href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"> -
-
- {{ $deployment->created_at }} UTC - > - {{ $deployment->status }} -
- @if (data_get($deployment, 'is_webhook') || data_get($deployment, 'pull_request_id')) -
- @if (data_get($deployment, 'is_webhook')) - Webhook - @endif - @if (data_get($deployment, 'pull_request_id')) - @if (data_get($deployment, 'is_webhook')) - | + ])> + +
+
+ data_get($deployment, 'status') === 'in_progress', + 'bg-purple-100/80 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300' => data_get($deployment, 'status') === 'queued', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200' => data_get($deployment, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => data_get($deployment, 'status') === 'finished', + 'bg-gray-100 text-gray-700 dark:bg-gray-600/30 dark:text-gray-300' => data_get($deployment, 'status') === 'cancelled-by-user', + ])> + @php + $statusText = match(data_get($deployment, 'status')) { + 'finished' => 'Success', + 'in_progress' => 'In Progress', + 'cancelled-by-user' => 'Cancelled', + 'queued' => 'Queued', + default => ucfirst(data_get($deployment, 'status')) + }; + @endphp + {{ $statusText }} + +
+ @if(data_get($deployment, 'status') !== 'queued') +
+ Started: {{ formatDateInServerTimezone(data_get($deployment, 'created_at'), data_get($application, 'destination.server')) }} + @if($deployment->status !== 'in_progress' && $deployment->status !== 'cancelled-by-user' && $deployment->status !== 'failed') +
Ended: {{ formatDateInServerTimezone(data_get($deployment, 'finished_at'), data_get($application, 'destination.server')) }} +
Duration: {{ calculateDuration(data_get($deployment, 'created_at'), data_get($deployment, 'finished_at')) }} + @elseif($deployment->status === 'in_progress') +
Running for: {{ calculateDuration(data_get($deployment, 'created_at'), now()) }} @endif - Pull Request #{{ data_get($deployment, 'pull_request_id') }} - @endif - @if (data_get($deployment, 'commit')) -
-
- @if ($deployment->commitMessage()) - ({{ data_get_str($deployment, 'commit')->limit(7) }} - - {{ $deployment->commitMessage() }}) - @else - {{ data_get_str($deployment, 'commit')->limit(7) }} - @endif -
-
- @endif -
- @else -
- @if (data_get($deployment, 'rollback') === true) - Rollback - @else - @if (data_get($deployment, 'is_api')) - API - @else - Manual - @endif - @endif - @if (data_get($deployment, 'commit')) -
-
- @if ($deployment->commitMessage()) - ({{ data_get_str($deployment, 'commit')->limit(7) }} - - {{ $deployment->commitMessage() }}) - @else - {{ data_get_str($deployment, 'commit')->limit(7) }} - @endif -
-
- @endif -
- @endif - @if (data_get($deployment, 'server_name') && $application->additional_servers->count() > 0) -
- Server: {{ data_get($deployment, 'server_name') }} -
- @endif -
- -
-
- @if ($deployment->status !== 'in_progress') - - @else - Running for 0s +
@endif +
+ @if (data_get($deployment, 'commit')) +
+
+ Commit: + + {{ substr(data_get($deployment, 'commit'), 0, 7) }} + + @if (!$deployment->commitMessage()) + + @if (data_get($deployment, 'is_webhook')) + Webhook + @if (data_get($deployment, 'pull_request_id')) + | Pull Request #{{ data_get($deployment, 'pull_request_id') }} + @endif + @elseif (data_get($deployment, 'pull_request_id')) + Pull Request #{{ data_get($deployment, 'pull_request_id') }} + @elseif (data_get($deployment, 'rollback') === true) + Rollback + @elseif (data_get($deployment, 'is_api')) + API + @else + Manual + @endif + + @endif + @if ($deployment->commitMessage()) + - + + {{ Str::before($deployment->commitMessage(), "\n") }} + + + + @if (data_get($deployment, 'is_webhook')) + Webhook + @if (data_get($deployment, 'pull_request_id')) + | Pull Request #{{ data_get($deployment, 'pull_request_id') }} + @endif + @elseif (data_get($deployment, 'pull_request_id')) + Pull Request #{{ data_get($deployment, 'pull_request_id') }} + @elseif (data_get($deployment, 'rollback') === true) + Rollback + @elseif (data_get($deployment, 'is_api')) + API + @else + Manual + @endif + + @endif +
+ @if ($deployment->commitMessage()) +
+ {{ Str::after($deployment->commitMessage(), "\n") }} +
+ @endif +
+ @endif +
+ + @if (data_get($deployment, 'server_name') && $application->additional_servers->count() > 0) +
+ Server: {{ data_get($deployment, 'server_name') }} +
+ @endif
-
+
@empty -
No deployments found
+
No deployments found
@endforelse - - @if ($deployments_count > 0) - - @endif
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index 7f8350a3e..2f46a4657 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -7,22 +7,40 @@
@forelse($executions as $execution)
data_get($execution, 'status') === 'success', - 'border-red-500' => data_get($execution, 'status') === 'failed', - 'border-yellow-500' => data_get($execution, 'status') === 'running', + 'flex flex-col border-l-2 transition-colors p-4 bg-white dark:bg-coolgray-100 text-black dark:text-white', + 'border-blue-500/50 border-dashed' => data_get($execution, 'status') === 'running', + 'border-error' => data_get($execution, 'status') === 'failed', + 'border-success' => data_get($execution, 'status') === 'success', ])> @if (data_get($execution, 'status') === 'running')
@endif -
Status: - {{ data_get($execution, 'status') }}
+
+ data_get($execution, 'status') === 'running', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => data_get($execution, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => data_get($execution, 'status') === 'success', + ])> + @php + $statusText = match(data_get($execution, 'status')) { + 'success' => 'Success', + 'running' => 'In Progress', + 'failed' => 'Failed', + default => ucfirst(data_get($execution, 'status')) + }; + @endphp + {{ $statusText }} + +
- Started At: {{ $this->formatDateInServerTimezone(data_get($execution, 'created_at')) }} + Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at'), $this->server()) }} + @if(data_get($execution, 'status') !== 'running') +
Ended: {{ formatDateInServerTimezone(data_get($execution, 'finished_at'), $this->server()) }} +
Duration: {{ calculateDuration(data_get($execution, 'created_at'), data_get($execution, 'finished_at')) }} + @endif
Database: {{ data_get($execution, 'database_name', 'N/A') }} diff --git a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php index c1400f5ec..63be05a2d 100644 --- a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php @@ -14,25 +14,41 @@ }"> @forelse($executions as $execution) - data_get($execution, 'id') == $selectedKey, - 'border-green-500' => data_get($execution, 'status') === 'success', - 'border-red-500' => data_get($execution, 'status') === 'failed', - 'border-yellow-500' => data_get($execution, 'status') === 'running', + 'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white', + 'bg-gray-200 dark:bg-coolgray-200' => data_get($execution, 'id') == $selectedKey, + 'border-blue-500/50 border-dashed' => data_get($execution, 'status') === 'running', + 'border-error' => data_get($execution, 'status') === 'failed', + 'border-success' => data_get($execution, 'status') === 'success', ])> - @if (data_get($execution, 'status') === 'running')
@endif -
Status: {{ data_get($execution, 'status') }} +
+ data_get($execution, 'status') === 'running', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => data_get($execution, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => data_get($execution, 'status') === 'success', + ])> + @php + $statusText = match(data_get($execution, 'status')) { + 'success' => 'Success', + 'running' => 'In Progress', + 'failed' => 'Failed', + default => ucfirst(data_get($execution, 'status')) + }; + @endphp + {{ $statusText }} +
- Started At: {{ $this->formatDateInServerTimezone(data_get($execution, 'created_at', now())) }} + Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at', now()), data_get($task, 'application.destination.server') ?? data_get($task, 'service.destination.server')) }} + @if(data_get($execution, 'status') !== 'running') +
Ended: {{ formatDateInServerTimezone(data_get($execution, 'finished_at'), data_get($task, 'application.destination.server') ?? data_get($task, 'service.destination.server')) }} +
Duration: {{ calculateDuration(data_get($execution, 'created_at'), data_get($execution, 'finished_at')) }} + @endif
@if (strlen($execution->message) > 0) diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 40ff6c8b5..b6dcf0ea0 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -27,67 +27,6 @@
-
-
-

Docker Cleanup

- -
-
- - @if (!$forceDockerCleanup) - - @endif -
- -
- -
-

- Warning: Enable these - options only if you fully understand their implications and - consequences!
Improper use will result in data loss and could cause - functional issues. -

-
- - -
-
-

Builds

Customize the build process.
diff --git a/resources/views/livewire/server/docker-cleanup-executions.blade.php b/resources/views/livewire/server/docker-cleanup-executions.blade.php new file mode 100644 index 000000000..95460c62c --- /dev/null +++ b/resources/views/livewire/server/docker-cleanup-executions.blade.php @@ -0,0 +1,127 @@ +
+ @forelse($executions as $execution) + data_get($execution, 'id') == $selectedKey, + 'border-blue-500/50 border-dashed' => data_get($execution, 'status') === 'running', + 'border-error' => data_get($execution, 'status') === 'failed', + 'border-success' => data_get($execution, 'status') === 'success', + ])> + @if (data_get($execution, 'status') === 'running') +
+ +
+ @endif +
+ data_get($execution, 'status') === 'running', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => data_get($execution, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => data_get($execution, 'status') === 'success', + ])> + @php + $statusText = match(data_get($execution, 'status')) { + 'success' => 'Success', + 'running' => 'In Progress', + 'failed' => 'Failed', + default => ucfirst(data_get($execution, 'status')) + }; + @endphp + {{ $statusText }} + +
+
+ Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at', now()), $server) }} + @if(data_get($execution, 'status') !== 'running') +
Ended: {{ formatDateInServerTimezone(data_get($execution, 'finished_at'), $server) }} +
Duration: {{ calculateDuration(data_get($execution, 'created_at'), data_get($execution, 'finished_at')) }} + @endif +
+
+ @if (strlen(data_get($execution, 'message', '')) > 0) +
+ + Download Logs + +
+ @endif + @if (data_get($execution, 'id') == $selectedKey) +
+
+ @if (data_get($execution, 'status') === 'running') +
+ Execution is running... + +
+ @endif + @if ($this->logLines->isNotEmpty()) +
+

Status Message:

+
+@foreach ($this->logLines as $line)
+{{ $line }}
+@endforeach
+
+
+ @if ($this->hasMoreLogs()) + + Load More + + @endif +
+
+ @else +
+
Status Message:
+
No output was recorded for this execution.
+
+ @endif + + @if (data_get($execution, 'cleanup_log')) +
+

Cleanup Log:

+ @foreach(json_decode(data_get($execution, 'cleanup_log'), true) as $result) +
+
+ + + + {{ data_get($result, 'command') }} +
+ @php + $output = data_get($result, 'output'); + $hasOutput = !empty(trim($output)); + @endphp +
+ @if($hasOutput) +
{{ $output }}
+ @else +

+ No output returned - command completed successfully +

+ @endif +
+
+ @endforeach +
+ @endif +
+
+ @endif + @empty +
No executions found.
+ @endforelse +
diff --git a/resources/views/livewire/server/docker-cleanup.blade.php b/resources/views/livewire/server/docker-cleanup.blade.php new file mode 100644 index 000000000..cdf48c6aa --- /dev/null +++ b/resources/views/livewire/server/docker-cleanup.blade.php @@ -0,0 +1,82 @@ +
+ + {{ data_get_str($server, 'name')->limit(10) }} > Docker Cleanup | Coolify + + +
+ +
+
+
+

Docker Cleanup

+
+
Configure Docker cleanup settings for your server.
+
+ +
+
+

Docker Cleanup

+ +
+
+ + @if (!$forceDockerCleanup) + + @endif +
+ +
+
+

+ Warning: Enable these + options only if you fully understand their implications and + consequences!
Improper use will result in data loss and could cause + functional issues. +

+
+ + +
+
+ +
+

Recent executions (click to check output)

+ +
+
+
+
diff --git a/routes/web.php b/routes/web.php index ec4923941..618e4e090 100644 --- a/routes/web.php +++ b/routes/web.php @@ -42,6 +42,7 @@ use App\Livewire\Server\Charts as ServerCharts; use App\Livewire\Server\CloudflareTunnels; use App\Livewire\Server\Delete as DeleteServer; use App\Livewire\Server\Destinations as ServerDestinations; +use App\Livewire\Server\DockerCleanup; use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\LogDrains; use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow; @@ -256,6 +257,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/proxy/dynamic', ProxyDynamicConfigurations::class)->name('server.proxy.dynamic-confs'); Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command'); + Route::get('/docker-cleanup', DockerCleanup::class)->name('server.docker-cleanup'); }); Route::get('/destinations', DestinationIndex::class)->name('destination.index'); Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');