// Logs Page Module class LogsPage { constructor() { this.logs = []; this.filters = { level: 'all', timeRange: '24h', search: '' }; this.init(); } async init() { // Load logs await this.loadLogs(); // Setup event listeners this.setupEventListeners(); // Setup WebSocket subscription for live logs this.setupWebSocketSubscription(); } async loadLogs() { try { // In a real app, this would fetch from /api/system/logs // Generate demo logs this.generateDemoLogs(50); this.applyFiltersAndRender(); } catch (error) { console.error('Error loading logs:', error); } } generateDemoLogs(count) { const levels = ['info', 'warn', 'error', 'debug']; const sources = ['server', 'database', 'auth', 'providers', 'clients', 'api']; const messages = [ 'Request processed successfully', 'Cache hit for model gpt-4', 'Rate limit check passed', 'High latency detected for DeepSeek provider', 'API key validation failed', 'Database connection pool healthy', 'New client registered: client-7', 'Backup completed successfully', 'Memory usage above 80% threshold', 'Provider Grok is offline', 'WebSocket connection established', 'Authentication token expired', 'Cost calculation completed', 'Rate limit exceeded for client-2', 'Database query optimization needed', 'SSL certificate renewed', 'System health check passed', 'Error in OpenAI API response', 'Gemini provider rate limited', 'DeepSeek connection timeout' ]; this.logs = []; const now = Date.now(); for (let i = 0; i < count; i++) { const level = levels[Math.floor(Math.random() * levels.length)]; const source = sources[Math.floor(Math.random() * sources.length)]; const message = messages[Math.floor(Math.random() * messages.length)]; // Generate timestamp (spread over last 24 hours) const hoursAgo = Math.random() * 24; const timestamp = new Date(now - hoursAgo * 60 * 60 * 1000); this.logs.push({ id: `log-${i}`, timestamp: timestamp.toISOString(), level: level, source: source, message: message, details: level === 'error' ? 'Additional error details would appear here' : null }); } // Sort by timestamp (newest first) this.logs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); } applyFiltersAndRender() { let filteredLogs = [...this.logs]; // Apply level filter if (this.filters.level !== 'all') { filteredLogs = filteredLogs.filter(log => log.level === this.filters.level); } // Apply time range filter const now = Date.now(); let timeLimit = now; switch (this.filters.timeRange) { case '1h': timeLimit = now - 60 * 60 * 1000; break; case '24h': timeLimit = now - 24 * 60 * 60 * 1000; break; case '7d': timeLimit = now - 7 * 24 * 60 * 60 * 1000; break; case '30d': timeLimit = now - 30 * 24 * 60 * 60 * 1000; break; } filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= timeLimit); // Apply search filter if (this.filters.search) { const searchLower = this.filters.search.toLowerCase(); filteredLogs = filteredLogs.filter(log => log.message.toLowerCase().includes(searchLower) || log.source.toLowerCase().includes(searchLower) || log.level.toLowerCase().includes(searchLower) ); } this.renderLogsTable(filteredLogs); } renderLogsTable(logs) { const tableBody = document.querySelector('#logs-table tbody'); if (!tableBody) return; if (logs.length === 0) { tableBody.innerHTML = `
No logs found matching your filters
`; return; } tableBody.innerHTML = logs.map(log => { const time = new Date(log.timestamp).toLocaleString(); const levelClass = `log-${log.level}`; const levelIcon = this.getLevelIcon(log.level); return ` ${time} ${log.level.toUpperCase()} ${log.source}
${log.message}
${log.details ? `
${log.details}
` : ''} `; }).join(''); // Add CSS for logs table this.addLogsStyles(); } getLevelIcon(level) { switch (level) { case 'error': return 'exclamation-circle'; case 'warn': return 'exclamation-triangle'; case 'info': return 'info-circle'; case 'debug': return 'bug'; default: return 'circle'; } } addLogsStyles() { const style = document.createElement('style'); style.textContent = ` .log-level-badge { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; } .log-error .log-level-badge { background-color: rgba(239, 68, 68, 0.1); color: var(--danger); } .log-warn .log-level-badge { background-color: rgba(245, 158, 11, 0.1); color: var(--warning); } .log-info .log-level-badge { background-color: rgba(6, 182, 212, 0.1); color: var(--info); } .log-debug .log-level-badge { background-color: rgba(100, 116, 139, 0.1); color: var(--text-secondary); } .log-message { font-size: 0.875rem; color: var(--text-primary); } .log-details { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; padding: 0.5rem; background-color: var(--bg-secondary); border-radius: 4px; border-left: 3px solid var(--danger); } .log-row:hover { background-color: var(--bg-hover); } .empty-table { text-align: center; padding: 3rem !important; color: var(--text-secondary); } .empty-table i { font-size: 2rem; margin-bottom: 1rem; opacity: 0.5; } .empty-table div { font-size: 0.875rem; } `; document.head.appendChild(style); } setupEventListeners() { // Filter controls const logFilter = document.getElementById('log-filter'); const timeRangeFilter = document.getElementById('log-time-range'); const searchInput = document.getElementById('log-search'); if (logFilter) { logFilter.addEventListener('change', (e) => { this.filters.level = e.target.value; this.applyFiltersAndRender(); }); } if (timeRangeFilter) { timeRangeFilter.addEventListener('change', (e) => { this.filters.timeRange = e.target.value; this.applyFiltersAndRender(); }); } if (searchInput) { let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.filters.search = e.target.value; this.applyFiltersAndRender(); }, 300); }); } // Action buttons const downloadBtn = document.getElementById('download-logs'); const clearBtn = document.getElementById('clear-logs'); if (downloadBtn) { downloadBtn.addEventListener('click', () => { this.downloadLogs(); }); } if (clearBtn) { clearBtn.addEventListener('click', () => { this.clearLogs(); }); } // Log row click for details document.addEventListener('click', (e) => { const logRow = e.target.closest('.log-row'); if (logRow) { this.showLogDetails(logRow.dataset.logId); } }); } setupWebSocketSubscription() { if (!window.wsManager) return; // Subscribe to log updates window.wsManager.subscribe('logs', (log) => { this.addNewLog(log); }); } addNewLog(log) { // Add to beginning of logs array this.logs.unshift({ id: `log-${Date.now()}`, timestamp: new Date().toISOString(), level: log.level || 'info', source: log.source || 'unknown', message: log.message || '', details: log.details || null }); // Keep logs array manageable if (this.logs.length > 1000) { this.logs = this.logs.slice(0, 1000); } // Apply filters and re-render this.applyFiltersAndRender(); } downloadLogs() { // Get filtered logs let filteredLogs = [...this.logs]; // Apply current filters if (this.filters.level !== 'all') { filteredLogs = filteredLogs.filter(log => log.level === this.filters.level); } // Create CSV content const headers = ['Timestamp', 'Level', 'Source', 'Message', 'Details']; const rows = filteredLogs.map(log => [ new Date(log.timestamp).toISOString(), log.level, log.source, `"${log.message.replace(/"/g, '""')}"`, log.details ? `"${log.details.replace(/"/g, '""')}"` : '' ]); const csvContent = [ headers.join(','), ...rows.map(row => row.join(',')) ].join('\n'); // Create download link const blob = new Blob([csvContent], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `llm-proxy-logs-${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); if (window.authManager) { window.authManager.showToast('Logs downloaded successfully', 'success'); } } clearLogs() { if (confirm('Are you sure you want to clear all logs? This action cannot be undone.')) { // In a real app, this would clear logs via API this.logs = []; this.applyFiltersAndRender(); if (window.authManager) { window.authManager.showToast('Logs cleared successfully', 'success'); } } } showLogDetails(logId) { const log = this.logs.find(l => l.id === logId); if (!log) return; // Show log details modal const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; document.body.appendChild(modal); // Setup event listeners const closeBtn = modal.querySelector('.modal-close'); const closeModalBtn = modal.querySelector('.close-modal'); const copyBtn = modal.querySelector('.copy-json'); const closeModal = () => { modal.classList.remove('active'); setTimeout(() => modal.remove(), 300); }; closeBtn.addEventListener('click', closeModal); closeModalBtn.addEventListener('click', closeModal); copyBtn.addEventListener('click', () => { const json = copyBtn.dataset.json; navigator.clipboard.writeText(json).then(() => { if (window.authManager) { window.authManager.showToast('JSON copied to clipboard', 'success'); } }).catch(err => { console.error('Failed to copy:', err); }); }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); // Add CSS for log details this.addLogDetailStyles(); } addLogDetailStyles() { const style = document.createElement('style'); style.textContent = ` .log-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; } .detail-item { display: flex; flex-direction: column; gap: 0.25rem; } .detail-item.full-width { grid-column: 1 / -1; } .detail-label { font-size: 0.875rem; font-weight: 600; color: var(--text-secondary); } .detail-value { font-size: 0.875rem; color: var(--text-primary); } .message-box { padding: 0.75rem; background-color: var(--bg-secondary); border-radius: 4px; border-left: 3px solid var(--primary); } .details-box { padding: 0.75rem; background-color: var(--bg-secondary); border-radius: 4px; border-left: 3px solid var(--warning); white-space: pre-wrap; font-family: monospace; font-size: 0.75rem; } .json-box { padding: 0.75rem; background-color: #1e293b; color: #e2e8f0; border-radius: 4px; overflow: auto; max-height: 300px; font-size: 0.75rem; line-height: 1.5; } `; document.head.appendChild(style); } refresh() { this.loadLogs(); if (window.authManager) { window.authManager.showToast('Logs refreshed', 'success'); } } } // Initialize logs page when needed window.initLogs = async () => { window.logsPage = new LogsPage(); }; // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = LogsPage; }