mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-18 15:20:40 +00:00
Fix deployment log follow feature stopping mid-deployment
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
<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"
|
||||
<input type="text" x-on:input="debouncedSearch($event.target.value)" :value="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">
|
||||
@@ -308,18 +316,26 @@
|
||||
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)
|
||||
@php
|
||||
// Limit rendered lines to prevent memory exhaustion
|
||||
$maxDisplayLines = 2000;
|
||||
$allLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== '');
|
||||
$totalLines = $allLines->count();
|
||||
$hasMoreLines = $totalLines > $maxDisplayLines;
|
||||
$displayLines = $hasMoreLines ? $allLines->slice(-$maxDisplayLines)->values() : $allLines;
|
||||
@endphp
|
||||
<div id="logs" class="font-mono max-w-full cursor-default">
|
||||
@if ($hasMoreLines)
|
||||
<div class="text-center py-2 text-gray-500 dark:text-gray-400 text-sm border-b dark:border-coolgray-300 mb-2">
|
||||
Showing last {{ number_format($maxDisplayLines) }} of {{ number_format($totalLines) }} lines
|
||||
</div>
|
||||
@endif
|
||||
<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)
|
||||
@foreach ($displayLines as $line)
|
||||
@php
|
||||
// Skip empty lines
|
||||
if (trim($line) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z)
|
||||
$timestamp = '';
|
||||
$logContent = $line;
|
||||
@@ -353,7 +369,7 @@
|
||||
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
|
||||
@endif
|
||||
<span data-line-text="{{ $logContent }}"
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
x-effect="searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
class="whitespace-pre-wrap break-all"></span>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
Reference in New Issue
Block a user