From 9493398b58ac39fc74d44d6bbae44336c93274e1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:30:26 +0100 Subject: [PATCH] Fix deployment logs flickering and HTML entity encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove per-line x-effect directives that re-evaluated for every log line during polling - Replace with efficient applySearch() function that updates logs once after Livewire morph - Remove unnecessary caching mechanisms (renderTrigger, decodeCache, matchCountCache) - Remove double HTML encoding of log lines (e() + Blade escaping) - Add decodeHtml() helper to properly decode HTML entities from data attributes - Use morph.updated hook instead of commit hook for efficient DOM updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- .../Project/Application/Deployment/Show.php | 11 +- .../application/deployment/show.blade.php | 153 +++++++----------- templates/service-templates-latest.json | 2 +- templates/service-templates.json | 2 +- 4 files changed, 62 insertions(+), 106 deletions(-) diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index b0f5df0c8..c204a49f1 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -105,16 +105,7 @@ public function polling() public function getLogLinesProperty() { - return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) { - $logLine['line'] = e($logLine['line']); - $logLine['line'] = preg_replace( - '/(https?:\/\/[^\s]+)/', - '$1', - $logLine['line'], - ); - - return $logLine; - }); + return decode_remote_command_output($this->application_deployment_queue); } public function copyLogs(): string diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 6aaf3e257..647f52128 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -11,13 +11,8 @@ rafId: null, showTimestamps: true, searchQuery: '', - renderTrigger: 0, + matchCount: 0, deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}', - // Cache for decoded HTML to avoid repeated DOMParser calls - decodeCache: new Map(), - // Cache for match count to avoid repeated DOM queries - matchCountCache: null, - lastSearchQuery: '', makeFullscreen() { this.fullscreen = !this.fullscreen; }, @@ -31,7 +26,6 @@ if (!this.alwaysScroll) return; this.rafId = requestAnimationFrame(() => { this.scrollToBottom(); - // Schedule next scroll after a reasonable delay (250ms instead of 100ms) if (this.alwaysScroll) { setTimeout(() => this.scheduleScroll(), 250); } @@ -48,10 +42,6 @@ } } }, - matchesSearch(text) { - if (!this.searchQuery.trim()) return true; - return text.toLowerCase().includes(this.searchQuery.toLowerCase()); - }, hasActiveLogSelection() { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !selection.toString().trim()) { @@ -63,86 +53,59 @@ return logsContainer.contains(range.commonAncestorContainer); }, decodeHtml(text) { - // Return cached result if available - if (this.decodeCache.has(text)) { - return this.decodeCache.get(text); - } - // Decode HTML entities with max iteration limit - let decoded = text; - let prev = ''; - let iterations = 0; - const maxIterations = 3; - - while (decoded !== prev && iterations < maxIterations) { - prev = decoded; - const doc = new DOMParser().parseFromString(decoded, 'text/html'); - decoded = doc.documentElement.textContent; - iterations++; - } - // Cache the result (limit cache size to prevent memory bloat) - if (this.decodeCache.size > 5000) { - // Clear oldest entries when cache gets too large - const firstKey = this.decodeCache.keys().next().value; - this.decodeCache.delete(firstKey); - } - this.decodeCache.set(text, decoded); - return decoded; + const doc = new DOMParser().parseFromString(text, 'text/html'); + return doc.documentElement.textContent; }, - renderHighlightedLog(el, text) { - // Skip re-render if user has text selected in logs - if (el.textContent && this.hasActiveLogSelection()) { - return; - } + highlightText(el, text, query) { + if (this.hasActiveLogSelection()) return; - const decoded = this.decodeHtml(text); el.textContent = ''; - - if (!this.searchQuery.trim()) { - el.textContent = decoded; - return; - } - - const query = this.searchQuery.toLowerCase(); - const lowerText = decoded.toLowerCase(); + const lowerText = text.toLowerCase(); let lastIndex = 0; - let index = lowerText.indexOf(query, lastIndex); + while (index !== -1) { if (index > lastIndex) { - el.appendChild(document.createTextNode(decoded.substring(lastIndex, index))); + el.appendChild(document.createTextNode(text.substring(lastIndex, index))); } const mark = document.createElement('span'); mark.className = 'log-highlight'; - mark.textContent = decoded.substring(index, index + this.searchQuery.length); + mark.textContent = text.substring(index, index + query.length); el.appendChild(mark); - - lastIndex = index + this.searchQuery.length; + lastIndex = index + query.length; index = lowerText.indexOf(query, lastIndex); } - if (lastIndex < decoded.length) { - el.appendChild(document.createTextNode(decoded.substring(lastIndex))); + if (lastIndex < text.length) { + el.appendChild(document.createTextNode(text.substring(lastIndex))); } }, - getMatchCount() { - if (!this.searchQuery.trim()) return 0; - // Return cached count if search query hasn't changed - if (this.lastSearchQuery === this.searchQuery && this.matchCountCache !== null) { - return this.matchCountCache; - } + applySearch() { const logs = document.getElementById('logs'); - if (!logs) return 0; + if (!logs) return; const lines = logs.querySelectorAll('[data-log-line]'); + const query = this.searchQuery.trim().toLowerCase(); let count = 0; - const query = this.searchQuery.toLowerCase(); + lines.forEach(line => { - if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(query)) { - count++; + const content = (line.dataset.logContent || '').toLowerCase(); + const textSpan = line.querySelector('[data-line-text]'); + const matches = !query || content.includes(query); + + line.classList.toggle('hidden', !matches); + if (matches && query) count++; + + if (textSpan) { + const originalText = this.decodeHtml(textSpan.dataset.lineText || ''); + if (!query) { + textSpan.textContent = originalText; + } else if (matches) { + this.highlightText(textSpan, originalText, query); + } } }); - this.matchCountCache = count; - this.lastSearchQuery = this.searchQuery; - return count; + + this.matchCount = query ? count : 0; }, downloadLogs() { const logs = document.getElementById('logs'); @@ -179,29 +142,31 @@ skip(); } }); - // Re-render logs after Livewire updates (debounced) - let renderTimeout = null; - const debouncedRender = () => { - clearTimeout(renderTimeout); - renderTimeout = setTimeout(() => { - this.matchCountCache = null; // Invalidate match cache on new content - this.renderTrigger++; - }, 100); - }; - document.addEventListener('livewire:navigated', () => { - this.$nextTick(debouncedRender); + + // Watch search query changes + this.$watch('searchQuery', () => { + this.applySearch(); }); - Livewire.hook('commit', ({ succeed }) => { - succeed(() => { - this.$nextTick(debouncedRender); - }); + + // Apply search after Livewire updates + Livewire.hook('morph.updated', ({ el }) => { + if (el.id === 'logs') { + this.$nextTick(() => { + this.applySearch(); + if (this.alwaysScroll) { + this.scrollToBottom(); + } + }); + } }); + // Stop auto-scroll when deployment finishes Livewire.on('deploymentFinished', () => { setTimeout(() => { this.stopScroll(); }, 500); }); + // Start auto-scroll if deployment is in progress if (this.alwaysScroll) { this.scheduleScroll(); @@ -229,7 +194,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-co {{ Str::headline(data_get($application_deployment_queue, 'status')) }} @endif -
@@ -324,7 +289,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar" :class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
-
No matches found.
@@ -334,19 +299,19 @@ class="text-gray-500 dark:text-gray-400 py-2"> $searchableContent = $line['timestamp'] . ' ' . $lineContent; @endphp
isset($line['command']) && $line['command'], 'flex gap-2 log-line', ])> {{ $line['timestamp'] }} - $line['hidden'], - 'text-red-500' => $line['stderr'], - 'font-bold' => isset($line['command']) && $line['command'], - 'whitespace-pre-wrap', - ]) - x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"> + $line['hidden'], + 'text-red-500' => $line['stderr'], + 'font-bold' => isset($line['command']) && $line['command'], + 'whitespace-pre-wrap', + ])>{{ $lineContent }}
@empty No logs yet. diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 5c482630b..c3e33b582 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -4350,7 +4350,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "analytics", "insights", diff --git a/templates/service-templates.json b/templates/service-templates.json index 226657fad..aae653dac 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -4350,7 +4350,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "analytics", "insights",