diff --git a/bot/static/index.html b/bot/static/index.html
index 66c2fa1..1443b59 100644
--- a/bot/static/index.html
+++ b/bot/static/index.html
@@ -27,6 +27,31 @@
overflow-y: scroll;
font-size: 0.85rem;
border-left: 2px solid #333;
+ position: relative;
+ }
+
+ #logs-content {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+
+ .log-line { line-height: 1.4; }
+ .log-line.log-error { color: #ff6b6b; }
+ .log-line.log-warning { color: #ffd93d; }
+ .log-line.log-info { color: #0f0; }
+ .log-line.log-debug { color: #888; }
+
+ .logs-paused-indicator {
+ position: sticky;
+ top: 0;
+ background: rgba(50, 50, 0, 0.9);
+ color: #ffd93d;
+ text-align: center;
+ padding: 0.25rem;
+ font-size: 0.75rem;
+ cursor: pointer;
+ z-index: 10;
+ display: none;
}
select, button, input {
@@ -1717,8 +1742,9 @@
-
+
Logs
+
⏸ Auto-scroll paused — click to resume
@@ -1867,6 +1893,7 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 DOMContentLoaded - initializing figurine subscribers list');
refreshFigurineSubscribers();
loadProfilePictureMetadata();
+ initLogsScrollDetection(); // Set up log auto-scroll tracking
// Set up periodic updates
setInterval(loadStatus, 10000);
@@ -3994,10 +4021,60 @@ async function loadLastPrompt() {
}
}
+// Log auto-scroll state
+let logsAutoScroll = true;
+
+function initLogsScrollDetection() {
+ const logsPanel = document.getElementById('logs-panel');
+ if (!logsPanel) return;
+ logsPanel.addEventListener('scroll', function() {
+ // If user is within 50px of the bottom, re-enable auto-scroll
+ const atBottom = logsPanel.scrollHeight - logsPanel.scrollTop - logsPanel.clientHeight < 50;
+ logsAutoScroll = atBottom;
+ const banner = document.getElementById('logs-paused-banner');
+ if (banner) banner.style.display = atBottom ? 'none' : 'block';
+ });
+}
+
+function scrollLogsToBottom() {
+ const logsPanel = document.getElementById('logs-panel');
+ if (logsPanel) {
+ logsPanel.scrollTop = logsPanel.scrollHeight;
+ logsAutoScroll = true;
+ const banner = document.getElementById('logs-paused-banner');
+ if (banner) banner.style.display = 'none';
+ }
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function classifyLogLine(line) {
+ const upper = line.toUpperCase();
+ if (upper.includes(' ERROR ') || upper.includes(' CRITICAL ') || upper.startsWith('ERROR') || upper.startsWith('CRITICAL') || upper.includes('TRACEBACK')) return 'log-error';
+ if (upper.includes(' WARNING ') || upper.startsWith('WARNING')) return 'log-warning';
+ if (upper.includes(' DEBUG ') || upper.startsWith('DEBUG')) return 'log-debug';
+ return 'log-info';
+}
+
async function loadLogs() {
try {
const result = await apiCall('/logs');
- document.getElementById('logs-content').textContent = result;
+ const logsContent = document.getElementById('logs-content');
+ const lines = (result || '').split('\n');
+ logsContent.innerHTML = lines.map(line => {
+ if (!line.trim()) return '';
+ const cls = classifyLogLine(line);
+ return `
${escapeHtml(line)}
`;
+ }).join('');
+
+ // Auto-scroll to bottom if user hasn't scrolled up
+ if (logsAutoScroll) {
+ scrollLogsToBottom();
+ }
} catch (error) {
console.error('Failed to load logs:', error);
}