diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 8504a160f..692138c5b 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -9,6 +9,7 @@ scrollDebounce: null, colorLogs: localStorage.getItem('coolify-color-logs') === 'true', searchQuery: '', + renderTrigger: 0, containerName: '{{ $container ?? "logs" }}', makeFullscreen() { this.fullscreen = !this.fullscreen; @@ -80,6 +81,18 @@ if (!this.searchQuery.trim()) return true; return line.toLowerCase().includes(this.searchQuery.toLowerCase()); }, + hasActiveLogSelection() { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || !selection.toString().trim()) { + return false; + } + const logsContainer = document.getElementById('logs'); + if (!logsContainer) return false; + + // Check if selection is within the logs container + const range = selection.getRangeAt(0); + return logsContainer.contains(range.commonAncestorContainer); + }, decodeHtml(text) { // Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS let decoded = text; @@ -96,6 +109,12 @@ return decoded; }, renderHighlightedLog(el, text) { + // Skip re-render if user has text selected in logs (preserves copy ability) + // But always render if the element is empty (initial render) + if (el.textContent && this.hasActiveLogSelection()) { + return; + } + const decoded = this.decodeHtml(text); el.textContent = ''; @@ -167,6 +186,12 @@ this.$wire.getLogs(true); this.logsLoaded = true; } + // Re-render logs after Livewire updates + Livewire.hook('commit', ({ succeed }) => { + succeed(() => { + this.$nextTick(() => { this.renderTrigger++; }); + }); + }); } }"> @if ($collapsible) @@ -350,8 +375,8 @@ class="text-gray-500 dark:text-gray-400 py-2"> @endphp
{{ $timestamp }} @endif
@endforeach