|
@@ -2406,3 +2406,253 @@ function initializeTableTypeConfig() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Application Logs Modal
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+let logsWebSocket = null;
|
|
|
|
|
+let logsEntries = [];
|
|
|
|
|
+const MAX_LOG_ENTRIES = 500;
|
|
|
|
|
+
|
|
|
|
|
+// Color classes for different log levels
|
|
|
|
|
+const LOG_LEVEL_COLORS = {
|
|
|
|
|
+ DEBUG: 'text-gray-500 dark:text-gray-400',
|
|
|
|
|
+ INFO: 'text-blue-600 dark:text-blue-400',
|
|
|
|
|
+ WARNING: 'text-amber-600 dark:text-amber-400',
|
|
|
|
|
+ ERROR: 'text-red-600 dark:text-red-400',
|
|
|
|
|
+ CRITICAL: 'text-red-700 dark:text-red-300 font-bold'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const LOG_LEVEL_BG = {
|
|
|
|
|
+ DEBUG: 'bg-gray-100 dark:bg-gray-700',
|
|
|
|
|
+ INFO: 'bg-blue-50 dark:bg-blue-900/30',
|
|
|
|
|
+ WARNING: 'bg-amber-50 dark:bg-amber-900/30',
|
|
|
|
|
+ ERROR: 'bg-red-50 dark:bg-red-900/30',
|
|
|
|
|
+ CRITICAL: 'bg-red-100 dark:bg-red-900/50'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function openLogsModal() {
|
|
|
|
|
+ const modal = document.getElementById('logsModal');
|
|
|
|
|
+ modal.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ // Load initial logs
|
|
|
|
|
+ loadInitialLogs();
|
|
|
|
|
+
|
|
|
|
|
+ // Connect to WebSocket for real-time updates
|
|
|
|
|
+ connectLogsWebSocket();
|
|
|
|
|
+
|
|
|
|
|
+ // Close on overlay click
|
|
|
|
|
+ modal.addEventListener('click', (e) => {
|
|
|
|
|
+ if (e.target === modal) {
|
|
|
|
|
+ closeLogsModal();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Close on Escape key
|
|
|
|
|
+ document.addEventListener('keydown', handleLogsEscapeKey);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeLogsModal() {
|
|
|
|
|
+ const modal = document.getElementById('logsModal');
|
|
|
|
|
+ modal.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ // Disconnect WebSocket
|
|
|
|
|
+ if (logsWebSocket) {
|
|
|
|
|
+ logsWebSocket.close();
|
|
|
|
|
+ logsWebSocket = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Remove event listener
|
|
|
|
|
+ document.removeEventListener('keydown', handleLogsEscapeKey);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleLogsEscapeKey(e) {
|
|
|
|
|
+ if (e.key === 'Escape') {
|
|
|
|
|
+ closeLogsModal();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadInitialLogs() {
|
|
|
|
|
+ const logsContent = document.getElementById('logsContent');
|
|
|
|
|
+ logsContent.innerHTML = '<div class="text-gray-500 dark:text-gray-400">Loading logs...</div>';
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/logs?limit=500');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ logsEntries = data.logs || [];
|
|
|
|
|
+ renderLogs();
|
|
|
|
|
+ updateLogsCount();
|
|
|
|
|
+
|
|
|
|
|
+ // Scroll to bottom
|
|
|
|
|
+ const container = document.getElementById('logsContainer');
|
|
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load logs:', error);
|
|
|
|
|
+ logsContent.innerHTML = '<div class="text-red-500">Failed to load logs</div>';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function connectLogsWebSocket() {
|
|
|
|
|
+ const statusEl = document.getElementById('logsConnectionStatus');
|
|
|
|
|
+
|
|
|
|
|
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
+ const wsUrl = `${protocol}//${window.location.host}/ws/logs`;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ logsWebSocket = new WebSocket(wsUrl);
|
|
|
|
|
+
|
|
|
|
|
+ logsWebSocket.onopen = () => {
|
|
|
|
|
+ statusEl.textContent = 'Live';
|
|
|
|
|
+ statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ logsWebSocket.onmessage = (event) => {
|
|
|
|
|
+ const message = JSON.parse(event.data);
|
|
|
|
|
+
|
|
|
|
|
+ if (message.type === 'log_entry') {
|
|
|
|
|
+ addLogEntry(message.data);
|
|
|
|
|
+ }
|
|
|
|
|
+ // Ignore heartbeat messages
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ logsWebSocket.onclose = () => {
|
|
|
|
|
+ statusEl.textContent = 'Disconnected';
|
|
|
|
|
+ statusEl.className = 'text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ logsWebSocket.onerror = (error) => {
|
|
|
|
|
+ console.error('Logs WebSocket error:', error);
|
|
|
|
|
+ statusEl.textContent = 'Error';
|
|
|
|
|
+ statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to connect to logs WebSocket:', error);
|
|
|
|
|
+ statusEl.textContent = 'Failed';
|
|
|
|
|
+ statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function addLogEntry(entry) {
|
|
|
|
|
+ // Add to beginning (newest first in memory, but we render oldest first in UI)
|
|
|
|
|
+ logsEntries.unshift(entry);
|
|
|
|
|
+
|
|
|
|
|
+ // Trim if exceeds max
|
|
|
|
|
+ if (logsEntries.length > MAX_LOG_ENTRIES) {
|
|
|
|
|
+ logsEntries.pop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Add to UI
|
|
|
|
|
+ const logsContent = document.getElementById('logsContent');
|
|
|
|
|
+ const entryEl = createLogEntryElement(entry);
|
|
|
|
|
+
|
|
|
|
|
+ // Insert at the end (bottom) for newest
|
|
|
|
|
+ logsContent.appendChild(entryEl);
|
|
|
|
|
+
|
|
|
|
|
+ // Auto-scroll if enabled
|
|
|
|
|
+ const autoScroll = document.getElementById('logsAutoScroll').checked;
|
|
|
|
|
+ if (autoScroll) {
|
|
|
|
|
+ const container = document.getElementById('logsContainer');
|
|
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Apply filter if active
|
|
|
|
|
+ const levelFilter = document.getElementById('logLevelFilter').value;
|
|
|
|
|
+ if (levelFilter && entry.level !== levelFilter) {
|
|
|
|
|
+ entryEl.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updateLogsCount();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createLogEntryElement(entry) {
|
|
|
|
|
+ const div = document.createElement('div');
|
|
|
|
|
+ div.className = `log-entry flex gap-2 py-1 px-2 rounded ${LOG_LEVEL_BG[entry.level] || 'bg-gray-50 dark:bg-gray-800'}`;
|
|
|
|
|
+ div.dataset.level = entry.level;
|
|
|
|
|
+
|
|
|
|
|
+ // Format timestamp (show only time part for brevity)
|
|
|
|
|
+ const timestamp = new Date(entry.timestamp);
|
|
|
|
|
+ const timeStr = timestamp.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
|
|
|
+ const msStr = timestamp.getMilliseconds().toString().padStart(3, '0');
|
|
|
|
|
+
|
|
|
|
|
+ div.innerHTML = `
|
|
|
|
|
+ <span class="text-gray-400 dark:text-gray-500 shrink-0">${timeStr}.${msStr}</span>
|
|
|
|
|
+ <span class="${LOG_LEVEL_COLORS[entry.level] || 'text-gray-600'} shrink-0 w-16">${entry.level}</span>
|
|
|
|
|
+ <span class="text-purple-600 dark:text-purple-400 shrink-0 truncate max-w-32" title="${entry.logger}:${entry.line}">${entry.module}:${entry.line}</span>
|
|
|
|
|
+ <span class="text-slate-700 dark:text-gray-200 break-all">${escapeHtml(entry.message)}</span>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ return div;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function escapeHtml(text) {
|
|
|
|
|
+ const div = document.createElement('div');
|
|
|
|
|
+ div.textContent = text;
|
|
|
|
|
+ return div.innerHTML;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderLogs() {
|
|
|
|
|
+ const logsContent = document.getElementById('logsContent');
|
|
|
|
|
+ logsContent.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ // Render in reverse order (oldest first, newest at bottom)
|
|
|
|
|
+ const reversedLogs = [...logsEntries].reverse();
|
|
|
|
|
+ const levelFilter = document.getElementById('logLevelFilter').value;
|
|
|
|
|
+
|
|
|
|
|
+ for (const entry of reversedLogs) {
|
|
|
|
|
+ const entryEl = createLogEntryElement(entry);
|
|
|
|
|
+ if (levelFilter && entry.level !== levelFilter) {
|
|
|
|
|
+ entryEl.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ logsContent.appendChild(entryEl);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function filterLogs() {
|
|
|
|
|
+ const levelFilter = document.getElementById('logLevelFilter').value;
|
|
|
|
|
+ const entries = document.querySelectorAll('#logsContent .log-entry');
|
|
|
|
|
+
|
|
|
|
|
+ let visibleCount = 0;
|
|
|
|
|
+ entries.forEach(entry => {
|
|
|
|
|
+ if (!levelFilter || entry.dataset.level === levelFilter) {
|
|
|
|
|
+ entry.classList.remove('hidden');
|
|
|
|
|
+ visibleCount++;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ entry.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Update count to show filtered amount
|
|
|
|
|
+ const countEl = document.getElementById('logsCount');
|
|
|
|
|
+ if (levelFilter) {
|
|
|
|
|
+ countEl.textContent = `${visibleCount} of ${logsEntries.length} entries (filtered)`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ countEl.textContent = `${logsEntries.length} entries`;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateLogsCount() {
|
|
|
|
|
+ const countEl = document.getElementById('logsCount');
|
|
|
|
|
+ const levelFilter = document.getElementById('logLevelFilter').value;
|
|
|
|
|
+
|
|
|
|
|
+ if (levelFilter) {
|
|
|
|
|
+ const filteredCount = logsEntries.filter(e => e.level === levelFilter).length;
|
|
|
|
|
+ countEl.textContent = `${filteredCount} of ${logsEntries.length} entries (filtered)`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ countEl.textContent = `${logsEntries.length} entries`;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function clearLogs() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fetch('/api/logs', { method: 'DELETE' });
|
|
|
|
|
+ logsEntries = [];
|
|
|
|
|
+ document.getElementById('logsContent').innerHTML = '<div class="text-gray-500 dark:text-gray-400 text-center py-4">Logs cleared</div>';
|
|
|
|
|
+ updateLogsCount();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to clear logs:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|