From a980fd460a2ef7ce7766171d034a8c645322299b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:04:17 +0100 Subject: [PATCH] feat(logs): Add loading indicator to download all logs buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visual feedback when downloading all logs in both container and deployment log views. Users now see an animated spinner and "Downloading..." text, preventing multiple concurrent downloads and improving UX during long operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- .../Project/Application/Deployment/Show.php | 19 +++++ app/Livewire/Project/Shared/GetLogs.php | 45 ++++++++++-- bootstrap/helpers/remoteProcess.php | 4 +- .../application/deployment/show.blade.php | 73 ++++++++++++++++--- .../project/shared/get-logs.blade.php | 30 ++++++-- templates/service-templates-latest.json | 6 +- templates/service-templates.json | 6 +- 7 files changed, 149 insertions(+), 34 deletions(-) diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index c204a49f1..954670582 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -121,6 +121,25 @@ public function copyLogs(): string return sanitizeLogsForExport($logs); } + public function downloadAllLogs(): string + { + $logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true) + ->map(function ($line) { + $prefix = ''; + if ($line['hidden']) { + $prefix = '[DEBUG] '; + } + if (isset($line['command']) && $line['command']) { + $prefix .= '[CMD]: '; + } + + return $line['timestamp'].' '.$prefix.trim($line['line']); + }) + ->join("\n"); + + return sanitizeLogsForExport($logs); + } + public function render() { return view('livewire.project.application.deployment.show'); diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 5f8046efd..22605e1bb 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -23,6 +23,8 @@ class GetLogs extends Component { public const MAX_LOG_LINES = 50000; + public const MAX_DOWNLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB + public string $outputs = ''; public string $errors = ''; @@ -164,10 +166,12 @@ public function getLogs($refresh = false) } // Collect new logs into temporary variable first to prevent flickering // (avoids clearing output before new data is ready) - $newOutputs = ''; - Process::run($sshCommand, function (string $type, string $output) use (&$newOutputs) { - $newOutputs .= removeAnsiColors($output); + // Use array accumulation + implode for O(n) instead of O(n²) string concatenation + $logChunks = []; + Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks) { + $logChunks[] = removeAnsiColors($output); }); + $newOutputs = implode('', $logChunks); if ($this->showTimeStamps) { $newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) { @@ -215,11 +219,40 @@ public function downloadAllLogs(): string $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); - $allLogs = ''; - Process::run($sshCommand, function (string $type, string $output) use (&$allLogs) { - $allLogs .= removeAnsiColors($output); + // Use array accumulation + implode for O(n) instead of O(n²) string concatenation + // Enforce 50MB size limit to prevent memory exhaustion from large logs + $logChunks = []; + $accumulatedBytes = 0; + $truncated = false; + + Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks, &$accumulatedBytes, &$truncated) { + if ($truncated) { + return; + } + + $output = removeAnsiColors($output); + $outputBytes = strlen($output); + + if ($accumulatedBytes + $outputBytes > self::MAX_DOWNLOAD_SIZE_BYTES) { + $remaining = self::MAX_DOWNLOAD_SIZE_BYTES - $accumulatedBytes; + if ($remaining > 0) { + $logChunks[] = substr($output, 0, $remaining); + } + $truncated = true; + + return; + } + + $logChunks[] = $output; + $accumulatedBytes += $outputBytes; }); + $allLogs = implode('', $logChunks); + + if ($truncated) { + $allLogs .= "\n\n[... Output truncated at 50MB limit ...]"; + } + if ($this->showTimeStamps) { $allLogs = str($allLogs)->split('/\n/')->sort(function ($a, $b) { $a = explode(' ', $a); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index fc70aa7da..217c82929 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -175,7 +175,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) throw new \RuntimeException($errorMessage, $exitCode); } -function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection +function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection { if (is_null($application_deployment_queue)) { return collect([]); @@ -216,7 +216,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d $seenCommands = collect(); $formatted = collect($decoded); - if (! $is_debug_enabled) { + if (! $is_debug_enabled && ! $includeAll) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); } diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 2b6afe75a..28872f4bc 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -173,7 +173,7 @@ class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300" :class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
+ class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
@@ -190,7 +190,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-co
-
+
+ - +
+ +
+
+ + +
+
+
+
+ class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
Lines: + class="input input-sm w-32 pl-11 dark:bg-coolgray-300" />
-
+
+ -
+
@@ -402,6 +415,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" /> +