From d9762e0310c7ca712a119572c1f3bdf87bf99b25 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:14:27 +0100 Subject: [PATCH] Fix deployment log follow feature stopping mid-deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed auto-disable behaviors that caused follow logs to stop unexpectedly: - Removed scroll detection that disabled following when user scrolled >50px from bottom - Removed fullscreen exit handler that disabled following - Removed ServiceChecked event listener that caused unnecessary flickers Follow logs now only stops when: - User explicitly clicks the Follow Logs button - Deployment finishes (auto-scrolls to end first, then disables after 500ms delay) Also improved get-logs component with memory optimizations: - Limited display to last 2000 lines to prevent memory exhaustion - Added debounced search (300ms) and scroll handling (150ms) - Optimized DOM rendering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Project/Application/Deployment/Show.php | 9 +-- .../application/deployment/show.blade.php | 41 ++++++------ .../project/shared/get-logs.blade.php | 62 ++++++++++++------- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 87f7cff8a..6d50fb3c7 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -22,10 +22,7 @@ class Show extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ServiceChecked" => '$refresh', 'refreshQueue', ]; } @@ -91,10 +88,14 @@ private function isKeepAliveOn() public function polling() { - $this->dispatch('deploymentFinished'); $this->application_deployment_queue->refresh(); $this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus(); $this->isKeepAliveOn(); + + // Dispatch event when deployment finishes to stop auto-scroll + if (! $this->isKeepAliveOn) { + $this->dispatch('deploymentFinished'); + } } public function getLogLinesProperty() diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index e5d1ce8e6..e685ae858 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -15,21 +15,14 @@ deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}', makeFullscreen() { this.fullscreen = !this.fullscreen; - if (this.fullscreen === false) { - this.alwaysScroll = false; - clearInterval(this.intervalId); - } }, - isScrolling: false, toggleScroll() { this.alwaysScroll = !this.alwaysScroll; 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); } else { @@ -37,17 +30,6 @@ this.intervalId = null; } }, - 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()); @@ -134,6 +116,18 @@ a.click(); URL.revokeObjectURL(url); }, + stopScroll() { + // Scroll to the end one final time before disabling + const logsContainer = document.getElementById('logsContainer'); + if (logsContainer) { + logsContainer.scrollTop = logsContainer.scrollHeight; + } + this.alwaysScroll = false; + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + }, init() { // Re-render logs after Livewire updates document.addEventListener('livewire:navigated', () => { @@ -144,14 +138,19 @@ this.$nextTick(() => { this.renderTrigger++; }); }); }); + // Stop auto-scroll when deployment finishes + Livewire.on('deploymentFinished', () => { + // Wait for DOM to update with final logs before scrolling to end + setTimeout(() => { + this.stopScroll(); + }, 500); + }); // 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); } @@ -254,7 +253,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- -
diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index b61887b6f..5c96e76ec 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -6,9 +6,10 @@ fullscreen: false, alwaysScroll: false, intervalId: null, + scrollDebounce: null, + searchTimeout: null, colorLogs: localStorage.getItem('coolify-color-logs') === 'true', searchQuery: '', - renderTrigger: 0, containerName: '{{ $container ?? "logs" }}', makeFullscreen() { this.fullscreen = !this.fullscreen; @@ -35,15 +36,22 @@ } }, handleScroll(event) { + // Skip if follow logs is disabled or this is a programmatic scroll 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; - } + + // Debounce scroll handling to avoid false positives from DOM mutations + // when Livewire re-renders and adds new log lines + clearTimeout(this.scrollDebounce); + this.scrollDebounce = setTimeout(() => { + const el = event.target; + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; + // Use larger threshold (100px) to avoid accidental disables + if (distanceFromBottom > 100) { + this.alwaysScroll = false; + clearInterval(this.intervalId); + this.intervalId = null; + } + }, 150); }, toggleColorLogs() { this.colorLogs = !this.colorLogs; @@ -73,6 +81,12 @@ if (!this.searchQuery.trim()) return true; return line.toLowerCase().includes(this.searchQuery.toLowerCase()); }, + debouncedSearch(query) { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + this.searchQuery = query; + }, 300); + }, decodeHtml(text) { // Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS let decoded = text; @@ -160,12 +174,6 @@ this.$wire.getLogs(true); this.logsLoaded = true; } - // Re-render logs after Livewire updates - Livewire.hook('commit', ({ succeed }) => { - succeed(() => { - this.$nextTick(() => { this.renderTrigger++; }); - }); - }); } }"> @if ($collapsible) @@ -216,7 +224,7 @@ class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"> -