{ this.isScrolling = false; }, 50); } }, scheduleScroll() { if (!this.alwaysScroll) return; this.rafId = requestAnimationFrame(() => { this.scrollToBottom(); if (this.alwaysScroll) { setTimeout(() => this.scheduleScroll(), 250); } }); }, toggleScroll() { this.alwaysScroll = !this.alwaysScroll; if (this.alwaysScroll) { this.scheduleScroll(); } else { if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; } } }, handleScroll(event) { if (!this.alwaysScroll || this.isScrolling) return; clearTimeout(this.scrollDebounce); this.scrollDebounce = setTimeout(() => { const el = event.target; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; if (distanceFromBottom > 100) { this.alwaysScroll = false; if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; } } }, 150); }, getLogLevel(content) { if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(content)) return 'error'; if (/\b(warn|warning|wrn|caution)\b/.test(content)) return 'warning'; if (/\b(debug|dbg|trace|verbose)\b/.test(content)) return 'debug'; return 'info'; }, toggleLogFilter(level) { this.logFilters[level] = !this.logFilters[level]; localStorage.setItem('coolify-log-filters', JSON.stringify(this.logFilters)); this.applySearch(); }, toggleColorLogs() { this.colorLogs = !this.colorLogs; localStorage.setItem('coolify-color-logs', this.colorLogs); this.applyColorLogs(); }, applyColorLogs() { const logs = document.getElementById('logs'); if (!logs) return; const lines = logs.querySelectorAll('[data-log-line]'); lines.forEach(line => { const content = (line.dataset.logContent || '').toLowerCase(); const level = this.getLogLevel(content); line.dataset.logLevel = level; line.classList.remove('log-error', 'log-warning', 'log-debug', 'log-info'); if (!this.colorLogs) return; line.classList.add('log-' + level); }); }, hasActiveLogSelection() { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !selection.toString().trim()) { return false; } const logsContainer = document.getElementById('logs'); if (!logsContainer) return false; const range = selection.getRangeAt(0); return logsContainer.contains(range.commonAncestorContainer); }, decodeHtml(text) { const doc = new DOMParser().parseFromString(text, 'text/html'); return doc.documentElement.textContent; }, applySearch() { const logs = document.getElementById('logs'); if (!logs) return; const lines = logs.querySelectorAll('[data-log-line]'); const query = this.searchQuery.trim().toLowerCase(); let count = 0; lines.forEach(line => { const content = (line.dataset.logContent || '').toLowerCase(); const textSpan = line.querySelector('[data-line-text]'); const level = line.dataset.logLevel || this.getLogLevel(content); const passesFilter = this.logFilters[level] !== false; const matchesSearch = !query || content.includes(query); const matches = passesFilter && matchesSearch; line.classList.toggle('hidden', !matches); if (matches && query) count++; // Update highlighting if (textSpan) { const originalText = this.decodeHtml(textSpan.dataset.lineText || ''); if (!query) { textSpan.textContent = originalText; } else if (matches) { this.highlightText(textSpan, originalText, query); } } }); this.matchCount = query ? count : 0; }, highlightText(el, text, query) { // Skip if user has selection if (this.hasActiveLogSelection()) return; el.textContent = ''; const lowerText = text.toLowerCase(); let lastIndex = 0; let index = lowerText.indexOf(query, lastIndex); while (index !== -1) { if (index > lastIndex) { el.appendChild(document.createTextNode(text.substring(lastIndex, index))); } const mark = document.createElement('span'); mark.className = 'log-highlight'; mark.textContent = text.substring(index, index + query.length); el.appendChild(mark); lastIndex = index + query.length; index = lowerText.indexOf(query, lastIndex); } if (lastIndex < text.length) { el.appendChild(document.createTextNode(text.substring(lastIndex))); } }, 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; } // Watch search query changes this.$watch('searchQuery', () => { this.applySearch(); }); // Handler for applying colors and search after DOM changes const applyAfterUpdate = () => { this.$nextTick(() => { this.applyColorLogs(); this.applySearch(); if (this.alwaysScroll) { this.scrollToBottom(); } }); }; // Apply colors after Livewire updates (existing content) Livewire.hook('morph.updated', ({ el }) => { if (el.id === 'logs') { applyAfterUpdate(); } }); // Apply colors after Livewire adds new content (initial load) Livewire.hook('morph.added', ({ el }) => { if (el.id === 'logs') { applyAfterUpdate(); } }); } }" @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 $displayLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== ''); @endphp
No matches found.
@foreach ($displayLines as $index => $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 for display: 2025-Dec-04 09:44:58 $timestamp = "{$year}-{$monthName}-{$day} {$time}"; // Include microseconds in key for uniqueness $lineKey = "{$timestamp}.{$microseconds}"; } @endphp
@if ($timestamp && $showTimeStamps) {{ $timestamp }} @endif {{ $logContent }}
@endforeach
@else
No logs yet.
@endif