{ const logsContainer = document.getElementById('logsContainer'); if (logsContainer) { this.isScrolling = true; logsContainer.scrollTop = logsContainer.scrollHeight; setTimeout(() => { this.isScrolling = false; }, 50); } }, 100); } else { clearInterval(this.intervalId); this.intervalId = null; } }, handleScroll(event) { // Skip if follow logs is disabled or this is a programmatic scroll if (!this.alwaysScroll || this.isScrolling) return; // 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; localStorage.setItem('coolify-color-logs', this.colorLogs); }, getLogLevel(text) { const lowerText = text.toLowerCase(); // Error detection (highest priority) if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(lowerText)) { return 'error'; } // Warning detection if (/\b(warn|warning|wrn|caution)\b/.test(lowerText)) { return 'warning'; } // Debug detection if (/\b(debug|dbg|trace|verbose)\b/.test(lowerText)) { return 'debug'; } // Info detection if (/\b(info|inf|notice)\b/.test(lowerText)) { return 'info'; } return null; }, matchesSearch(line) { 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; let prev = ''; let iterations = 0; const maxIterations = 3; // Prevent DoS from deeply nested HTML entities while (decoded !== prev && iterations < maxIterations) { prev = decoded; const doc = new DOMParser().parseFromString(decoded, 'text/html'); decoded = doc.documentElement.textContent; iterations++; } 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 = ''; if (!this.searchQuery.trim()) { el.textContent = decoded; return; } const query = this.searchQuery.toLowerCase(); const lowerText = decoded.toLowerCase(); let lastIndex = 0; let index = lowerText.indexOf(query, lastIndex); while (index !== -1) { // Add text before match if (index > lastIndex) { el.appendChild(document.createTextNode(decoded.substring(lastIndex, index))); } // Add highlighted match const mark = document.createElement('span'); mark.className = 'log-highlight'; mark.textContent = decoded.substring(index, index + this.searchQuery.length); el.appendChild(mark); lastIndex = index + this.searchQuery.length; index = lowerText.indexOf(query, lastIndex); } // Add remaining text if (lastIndex < decoded.length) { el.appendChild(document.createTextNode(decoded.substring(lastIndex))); } }, getMatchCount() { if (!this.searchQuery.trim()) return 0; const logs = document.getElementById('logs'); if (!logs) return 0; const lines = logs.querySelectorAll('[data-log-line]'); let count = 0; lines.forEach(line => { if (line.textContent.toLowerCase().includes(this.searchQuery.toLowerCase())) { count++; } }); return count; }, downloadLogs() { const logs = document.getElementById('logs'); if (!logs) return; const visibleLines = logs.querySelectorAll('[data-log-line]:not(.hidden)'); let content = ''; visibleLines.forEach(line => { const text = line.textContent.replace(/\s+/g, ' ').trim(); if (text) { content += text + String.fromCharCode(10); } }); const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const timestamp = new Date().toISOString().slice(0,19).replace(/[T:]/g, '-'); a.download = this.containerName + '-logs-' + timestamp + '.txt'; a.click(); URL.revokeObjectURL(url); }, init() { if (this.expanded) { this.$wire.getLogs(true); this.logsLoaded = true; } // Prevent Livewire from morphing logs container when text is selected Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => { if (el.id === 'logs' && this.hasActiveLogSelection()) { skip(); } }); // Re-render logs after Livewire updates Livewire.hook('commit', ({ succeed }) => { succeed(() => { this.$nextTick(() => { this.renderTrigger++; }); }); }); } }" @keydown.window="handleKeyDown($event)"> @if ($collapsible)
@if ($displayName)

{{ $displayName }}

@elseif ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))

{{ $container }}

@else

{{ str($container)->beforeLast('-')->headline() }}

@endif @if ($pull_request)
({{ $pull_request }})
@endif @if ($streamLogs) @endif
@endif
Lines:
@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
@if ($hasMoreLines)
Showing last {{ number_format($maxDisplayLines) }} of {{ number_format($totalLines) }} lines
@endif
No matches found.
@foreach ($displayLines as $line) @php // Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z) $timestamp = ''; $logContent = $line; if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z?\s*(.*)$/', $line, $matches)) { $year = $matches[1]; $month = $matches[2]; $day = $matches[3]; $time = $matches[4]; $microseconds = isset($matches[5]) ? substr($matches[5], 0, 6) : '000000'; $logContent = $matches[6]; // Convert month number to abbreviated name $monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; $monthName = $monthNames[(int)$month - 1] ?? $month; // Format: 2025-Dec-04 09:44:58.198879 $timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}"; } @endphp
@if ($timestamp && $showTimeStamps) {{ $timestamp }} @endif
@endforeach
@else
No logs yet.
@endif