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",