Add auto-scroll and colorized log levels for deployment logs

- Enable auto-scroll by default when deployment is in progress
- Fix scroll direction to scroll to bottom (where new logs appear)
- Show Follow Logs button outside fullscreen mode
- Add colorized log levels with localStorage toggle:
  - Error (red): error, failed, exception, fatal, panic, critical
  - Warning (yellow): warn, warning, caution
  - Debug (purple): debug, trace, verbose
  - Info (blue): info, notice
- Colors use subtle opacity (10-15%) for readability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-05 09:39:39 +01:00
parent 558a885fdc
commit 4f2bb3b50e

View file

@ -7,9 +7,10 @@
<livewire:project.application.heading :application="$application" />
<div x-data="{
fullscreen: false,
alwaysScroll: false,
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
intervalId: null,
showTimestamps: true,
colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
searchQuery: '',
renderTrigger: 0,
deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}',
@ -28,7 +29,7 @@
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
this.isScrolling = true;
logsContainer.scrollTop = 0;
logsContainer.scrollTop = logsContainer.scrollHeight;
setTimeout(() => { this.isScrolling = false; }, 50);
}
}, 100);
@ -40,14 +41,38 @@
handleScroll(event) {
if (!this.alwaysScroll || this.isScrolling) return;
const el = event.target;
// With flex-col-reverse, scrollTop is 0 at visual top and goes negative when scrolling down
const isAtTop = Math.abs(el.scrollTop) < 50;
if (!isAtTop) {
// Check if user scrolled away from the bottom
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (distanceFromBottom > 50) {
this.alwaysScroll = false;
clearInterval(this.intervalId);
this.intervalId = null;
}
},
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(text) {
if (!this.searchQuery.trim()) return true;
return text.toLowerCase().includes(this.searchQuery.toLowerCase());
@ -144,6 +169,17 @@
this.$nextTick(() => { this.renderTrigger++; });
});
});
// Start auto-scroll if deployment is in progress
if (this.alwaysScroll) {
this.intervalId = setInterval(() => {
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
this.isScrolling = true;
logsContainer.scrollTop = logsContainer.scrollHeight;
setTimeout(() => { this.isScrolling = false; }, 50);
}
}, 100);
}
}
}">
<livewire:project.application.deployment-navbar
@ -214,7 +250,16 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
</svg>
</button>
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? '!text-warning' : ''"
<button title="Toggle Log Colors" x-on:click="toggleColorLogs"
:class="colorLogs ? '!text-warning' : ''"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z" />
</svg>
</button>
<button title="Follow Logs" :class="alwaysScroll ? '!text-warning' : ''"
x-on:click="toggleScroll"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -257,9 +302,15 @@ class="text-gray-500 dark:text-gray-400 py-2">
$searchableContent = $line['timestamp'] . ' ' . $lineContent;
@endphp
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
x-bind:class="{ 'hidden': !matchesSearch($el.dataset.logContent) }" @class([
x-bind:class="{
'hidden': !matchesSearch($el.dataset.logContent),
'bg-red-500/10 dark:bg-red-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'error',
'bg-yellow-500/10 dark:bg-yellow-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'warning',
'bg-purple-500/10 dark:bg-purple-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'debug',
'bg-blue-500/10 dark:bg-blue-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'info',
}" @class([
'mt-2' => isset($line['command']) && $line['command'],
'flex gap-2 dark:hover:bg-coolgray-500 hover:bg-gray-100',
'flex gap-2',
])>
<span x-show="showTimestamps"
class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>