UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

1,388 lines (1,220 loc) โ€ข 42.2 kB
/** * LiveLogsTab - Vanilla JavaScript component for live logs monitoring * Follows the same pattern as other tab components in the codebase */ export class LiveLogsTab { // CSS styles for the component static getStyles() { return ` .live-logs-container .header { background: white; border-radius: 8px; padding: 24px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; text-align: left; } .live-logs-container .header h1 { color: #1a202c; font-size: 28px; font-weight: 700; margin-bottom: 8px; } .live-logs-container .header p { color: #64748b; font-size: 16px; margin-bottom: 16px; } .live-logs-container .system-status { display: inline-flex; align-items: center; gap: 8px; background: #10b981; color: white; padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 12px; } .live-logs-container .system-status.connecting { background: #f59e0b; animation: pulse 1.5s infinite; } .live-logs-container .system-status.disconnected { background: #ef4444; } .live-logs-container .status-dot { width: 6px; height: 6px; background: white; border-radius: 50%; animation: pulse 2s infinite; } .live-logs-container .status-dot.connecting { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .live-logs-container .time-window-controls { background: white; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } .live-logs-container .time-window-controls label { display: flex; align-items: center; gap: 8px; color: #374151; font-size: 14px; font-weight: 500; } .live-logs-container .time-window-controls select { padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 4px; background: white; font-size: 13px; color: #374151; } .live-logs-container .status { display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; } .live-logs-container .status.connected { background: #dcfce7; color: #166534; } .live-logs-container .status.disconnected { background: #fef2f2; color: #dc2626; } .live-logs-container .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } .live-logs-container .status.connected .status-indicator { background: #10b981; animation: pulse 2s infinite; } .live-logs-container .status.disconnected .status-indicator { background: #ef4444; } .live-logs-container .cleanup-notice { background: #fef3c7; border: 1px solid #fed7aa; color: #d97706; padding: 8px 12px; border-radius: 6px; font-size: 12px; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; } .live-logs-container .cleanup-notice.hidden { display: none; } .live-logs-container .cleanup-notice .icon { font-size: 14px; } .live-logs-container .kpi-filters { background: white; border-radius: 8px; padding: 16px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } .live-logs-container .kpi-filter-group { display: flex; flex-direction: column; gap: 4px; } .live-logs-container .kpi-filter-group label { font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; } .live-logs-container .kpi-filter-group select { padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 4px; background: white; font-size: 13px; color: #374151; min-width: 140px; } .live-logs-container .kpi-filter-info { display: flex; gap: 8px; align-items: center; margin-left: auto; } .live-logs-container .filter-badge { background: #eff6ff; color: #1e40af; padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; border: 1px solid #dbeafe; } .live-logs-container .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; } .live-logs-container .kpi-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .live-logs-container .kpi-label { color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; font-weight: 600; } .live-logs-container .kpi-value { color: #1a202c; font-size: 32px; font-weight: 700; margin-bottom: 8px; transition: color 0.3s ease; } .live-logs-container .kpi-value.updating { color: #5897de; } .live-logs-container .kpi-change { color: #64748b; font-size: 13px; } .live-logs-container .controls { background: white; border-radius: 8px; padding: 20px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .live-logs-container .controls-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px; } .live-logs-container .control-group { display: flex; flex-direction: column; gap: 8px; } .live-logs-container .control-group label { color: #374151; font-size: 12px; font-weight: 600; } .live-logs-container .control-group select, .live-logs-container .control-group input { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 14px; } .live-logs-container .control-actions { display: flex; gap: 12px; align-items: center; } .live-logs-container .btn { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .live-logs-container .btn-primary { background: #3b82f6; color: white; } .live-logs-container .btn-primary:hover { background: #2563eb; } .live-logs-container .btn-secondary { background: #6b7280; color: white; } .live-logs-container .btn-secondary:hover { background: #4b5563; } .live-logs-container .btn-outline { background: white; color: #374151; border: 1px solid #d1d5db; } .live-logs-container .btn-outline:hover { background: #f9fafb; } .live-logs-container .events-container { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .live-logs-container .events-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #e2e8f0; } .live-logs-container .events-title { color: #1a202c; font-size: 18px; font-weight: 600; } .live-logs-container .events-tabs { display: flex; gap: 8px; } .live-logs-container .events-tabs .tab { padding: 6px 12px; background: white; border: 1px solid #e2e8f0; border-radius: 4px; color: #64748b; cursor: pointer; transition: all 0.2s ease; font-size: 13px; font-weight: 500; } .live-logs-container .events-tabs .tab.active { background: #022d54; color: white; border-color: #022d54; } .live-logs-container .events-tabs .tab:hover:not(.active) { border-color: #cbd5e0; background: #f8fafc; } .live-logs-container .events-list { max-height: 500px; overflow-y: auto; padding-right: 8px; } .live-logs-container .events-list::-webkit-scrollbar { width: 6px; } .live-logs-container .events-list::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 3px; } .live-logs-container .events-list::-webkit-scrollbar-thumb { background: #cbd5e0; border-radius: 3px; } .live-logs-container .events-list::-webkit-scrollbar-thumb:hover { background: #94a3b8; } .live-logs-container .event-item { background: white; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin-bottom: 12px; border-left: 3px solid transparent; transition: all 0.2s ease; animation: slideIn 0.3s ease; } @keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .live-logs-container .event-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .live-logs-container .event-type { padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .live-logs-container .event-type.operations { background: #eff6ff; color: #1e40af; } .live-logs-container .event-type.management { background: #f0fdf4; color: #166534; } .live-logs-container .event-type.admin { background: #fef3c7; color: #d97706; } .live-logs-container .event-type.error { background: #fef2f2; color: #dc2626; } .live-logs-container .event-time { color: #9ca3af; font-size: 12px; } .live-logs-container .event-content { color: #374151; font-size: 14px; margin-bottom: 8px; line-height: 1.5; } .live-logs-container .event-meta { display: flex; gap: 16px; flex-wrap: wrap; } .live-logs-container .meta-item { color: #6b7280; font-size: 12px; } .live-logs-container .meta-item strong { color: #374151; } .live-logs-container .loading { text-align: center; color: #6b7280; padding: 40px; font-size: 14px; } .live-logs-container .no-events { text-align: center; color: #9ca3af; padding: 40px; font-size: 14px; font-style: italic; } .live-logs-container .error-message { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 16px; border-radius: 6px; margin: 16px 0; font-size: 14px; line-height: 1.5; } `; } constructor(botId, managementId, managementSecret, apiBaseUrl) { this.botId = botId; this.managementId = managementId; this.managementSecret = managementSecret; this.apiBaseUrl = apiBaseUrl; this.logs = []; this.isConnected = false; this.isConnecting = false; this.isLoading = false; this.error = null; this.failedAttempts = 0; this.maxFailedAttempts = 3; this.activeTab = 'all'; this.filters = { duration: 300, function: '', severity: '', }; this.timeWindow = 3600; // 1 hour default this.cleanupNotice = { show: false, count: 0 }; // Metrics state this.metrics = { totalOperations: 0, totalManagement: 0, averageLatency: 0, totalTokens: 0, errorCount: 0, successCount: 0, startTime: Date.now(), }; // Filtered metrics state this.filteredMetrics = { operations: { total: 0, averageLatency: 0, errorCount: 0, successCount: 0, totalTokens: 0, }, management: { total: 0, averageLatency: 0, errorCount: 0, successCount: 0, totalTokens: 0, }, byFunction: {}, }; // KPI filter state this.kpiFilter = { category: 'all', // 'all', 'operations', 'management' function: 'all', // 'all' or specific function name }; this.pollingInterval = null; this.container = null; // Function name mappings for user-friendly display this.functionNameMap = { // Bot Operations generate_response_endpoint: 'Message Processing', create_session_endpoint: 'Session Creation', download_conversation: 'Conversation Export', delete_conversation_endpoint: 'Conversation Cleanup', // Bot Management create_bot: 'Bot Creation', add_knowledge: 'Knowledge Addition', update_knowledge: 'Knowledge Update', delete_knowledge: 'Knowledge Removal', add_web_knowledge: 'Web Knowledge Import', list_knowledge: 'Knowledge Listing', get_config_json: 'Configuration Retrieval', update_config: 'Configuration Update', get_session_analytics: 'Analytics Query', search_conversation_analytics: 'Conversation Search', get_bot_costs_summary: 'Cost Analysis', get_bot_costs_detailed: 'Detailed Cost Report', upload_conversation: 'Conversation Import', view_conversation: 'Conversation View', translate_welcome_messages: 'Message Translation', refresh_website: 'Website Refresh', get_website_pages: 'Website Analysis', // Admin Operations create_token: 'Token Creation', delete_token: 'Token Deletion', get_active_bots: 'Active Bots Query', get_conversation_timeline: 'Timeline Analysis', get_conversation_ids: 'Conversation IDs', get_conversation_detail: 'Conversation Details', get_costs_by_bot: 'Bot Cost Analysis', // Demo Operations add_demo_knowledge: 'Demo Knowledge Addition', create_demo_session: 'Demo Session Creation', generate_demo_response: 'Demo Response Generation', delete_demo_conversation: 'Demo Conversation Cleanup', operator_create_demo_bot: 'Demo Bot Creation', }; } /** * Initialize the component with a container element * @param {HTMLElement} container - The container element */ init(container) { console.log('๐Ÿ”ง LiveLogsTab.init() called with container:', container); this.container = container; this.render(); this.setupEventListeners(); console.log('โœ… LiveLogsTab initialized successfully'); // Don't auto-start polling - user must manually trigger it } /** * Render the component */ render() { console.log('๐ŸŽจ LiveLogsTab.render() called'); if (!this.container) { console.warn('โŒ No container found for LiveLogsTab'); return; } console.log('๐Ÿ“ Setting container innerHTML'); this.container.innerHTML = this.getHTML(); console.log('โœ… LiveLogsTab rendered successfully'); } /** * Get filtered metrics for display */ getFilteredMetrics() { return this.filteredMetrics; } /** * Generate the HTML for the live logs tab * @returns {string} Live logs tab HTML string */ getHTML() { return ` <style>${LiveLogsTab.getStyles()}</style> <div class="live-logs-container"> <div class="header"> <h1>Live Activity Monitor</h1> <p>Track bot operations and management activities in real-time</p> <div class="system-status ${this.isConnected ? '' : this.isConnecting ? 'connecting' : 'disconnected'}"> <span class="status-dot ${this.isConnecting ? 'connecting' : ''}"></span> ${this.isConnecting ? 'Connecting...' : this.isConnected ? 'System Active' : 'Disconnected'} </div> </div> <!-- Time Window Controls --> <div class="time-window-controls"> <label> Time Window: <select id="timeWindow"> <option value="300" ${this.timeWindow === 300 ? 'selected' : ''}>5 minutes</option> <option value="600" ${this.timeWindow === 600 ? 'selected' : ''}>10 minutes</option> <option value="900" ${this.timeWindow === 900 ? 'selected' : ''}>15 minutes</option> <option value="1800" ${this.timeWindow === 1800 ? 'selected' : ''}>30 minutes</option> <option value="3600" ${this.timeWindow === 3600 ? 'selected' : ''}>1 hour</option> </select> </label> <div class="status ${this.isConnected ? 'connected' : 'disconnected'}"> <span class="status-indicator"></span> ${this.isConnected ? 'Connected โ€ข Auto-cleanup active' : 'Disconnected'} </div> <div style="margin-left: auto; font-size: 12px; color: #6b7280;"> Events older than <strong>${this.timeWindow === 300 ? '5 minutes' : this.timeWindow === 600 ? '10 minutes' : this.timeWindow === 900 ? '15 minutes' : this.timeWindow === 1800 ? '30 minutes' : '1 hour'}</strong> are automatically removed </div> </div> <!-- Cleanup Notice --> <div class="cleanup-notice ${this.cleanupNotice.show ? '' : 'hidden'}" id="cleanupNotice"> <span class="icon">๐Ÿงน</span> <span>Cleaned up <strong>${this.cleanupNotice.count} old events</strong> to maintain performance</span> </div> <!-- KPI Filter Controls --> <div class="kpi-filters"> <div class="kpi-filter-group"> <label>Category Filter:</label> <select id="categoryFilter"> <option value="all" ${this.kpiFilter.category === 'all' ? 'selected' : ''}>All Categories</option> <option value="operations" ${this.kpiFilter.category === 'operations' ? 'selected' : ''}>Operations Only</option> <option value="management" ${this.kpiFilter.category === 'management' ? 'selected' : ''}>Management Only</option> </select> </div> <div class="kpi-filter-group"> <label>Action Filter:</label> <select id="functionFilter"> <option value="all" ${this.kpiFilter.function === 'all' ? 'selected' : ''}>All Actions</option> <option value="generate_response_endpoint" ${this.kpiFilter.function === 'generate_response_endpoint' ? 'selected' : ''}>Message Processing</option> <option value="create_session_endpoint" ${this.kpiFilter.function === 'create_session_endpoint' ? 'selected' : ''}>Session Creation</option> <option value="add_knowledge" ${this.kpiFilter.function === 'add_knowledge' ? 'selected' : ''}>Knowledge Addition</option> <option value="update_config" ${this.kpiFilter.function === 'update_config' ? 'selected' : ''}>Configuration Update</option> </select> </div> <div class="kpi-filter-info"> <span class="filter-badge">All Metrics</span> </div> </div> <div class="kpis"> <div class="kpi-card"> <div class="kpi-label">Total Operations</div> <div class="kpi-value" id="totalOperations">${this.filteredMetrics.operations.total + this.filteredMetrics.management.total}</div> <div class="kpi-change"> ${Math.floor((this.filteredMetrics.operations.total + this.filteredMetrics.management.total) / (this.timeWindow / 3600))} per hour โ€ข ${this.filteredMetrics.operations.total + this.filteredMetrics.management.total} total </div> </div> <div class="kpi-card"> <div class="kpi-label">Average Latency</div> <div class="kpi-value" id="avgLatency">${this.filteredMetrics.operations.averageLatency.toFixed(0)}ms</div> <div class="kpi-change"> Response time performance </div> </div> <div class="kpi-card"> <div class="kpi-label">Success Rate</div> <div class="kpi-value" id="successRate">${this.filteredMetrics.operations.successCount + this.filteredMetrics.management.successCount > 0 ? Math.round(((this.filteredMetrics.operations.successCount + this.filteredMetrics.management.successCount) / (this.filteredMetrics.operations.total + this.filteredMetrics.management.total)) * 100) : 100}%</div> <div class="kpi-change"> ${this.filteredMetrics.operations.errorCount + this.filteredMetrics.management.errorCount} errors โ€ข ${this.filteredMetrics.operations.successCount + this.filteredMetrics.management.successCount} successful </div> </div> <div class="kpi-card"> <div class="kpi-label">Total Tokens</div> <div class="kpi-value" id="totalTokens">${(this.filteredMetrics.operations.totalTokens + this.filteredMetrics.management.totalTokens).toLocaleString()}</div> <div class="kpi-change"> Token usage for filtered operations </div> </div> </div> <div class="controls"> <div class="controls-grid"> <div class="control-group"> <label>Duration:</label> <select id="durationFilter"> <option value="60" ${this.filters.duration === 60 ? 'selected' : ''}>1 minute</option> <option value="300" ${this.filters.duration === 300 ? 'selected' : ''}>5 minutes</option> <option value="600" ${this.filters.duration === 600 ? 'selected' : ''}>10 minutes</option> <option value="1800" ${this.filters.duration === 1800 ? 'selected' : ''}>30 minutes</option> </select> </div> <div class="control-group"> <label>Function:</label> <select id="functionFilter2"> <option value="" ${this.filters.function === '' ? 'selected' : ''}>All Functions</option> <option value="generate_response_endpoint" ${this.filters.function === 'generate_response_endpoint' ? 'selected' : ''}>Message Processing</option> <option value="create_session_endpoint" ${this.filters.function === 'create_session_endpoint' ? 'selected' : ''}>Session Creation</option> <option value="add_knowledge" ${this.filters.function === 'add_knowledge' ? 'selected' : ''}>Knowledge Addition</option> <option value="update_config" ${this.filters.function === 'update_config' ? 'selected' : ''}>Configuration Update</option> </select> </div> <div class="control-group"> <label>Severity:</label> <select id="severityFilter"> <option value="" ${this.filters.severity === '' ? 'selected' : ''}>All Levels</option> <option value="ERROR" ${this.filters.severity === 'ERROR' ? 'selected' : ''}>Errors Only</option> <option value="WARNING" ${this.filters.severity === 'WARNING' ? 'selected' : ''}>Warnings Only</option> <option value="INFO" ${this.filters.severity === 'INFO' ? 'selected' : ''}>Info Only</option> </select> </div> </div> <div class="control-actions"> <button class="btn btn-primary" id="start-monitoring">Start Monitoring</button> <button class="btn btn-secondary" id="stop-monitoring" style="display: none;">Stop Monitoring</button> <button class="btn btn-outline" id="refreshLogs">Refresh</button> <button class="btn btn-outline" id="clearLogs">Clear</button> </div> </div> <div class="events-container"> <div class="events-header"> <h2 class="events-title">Live Events</h2> <div class="events-tabs"> <button class="tab ${this.activeTab === 'all' ? 'active' : ''}" data-tab="all">All</button> <button class="tab ${this.activeTab === 'operations' ? 'active' : ''}" data-tab="operations">Operations</button> <button class="tab ${this.activeTab === 'management' ? 'active' : ''}" data-tab="management">Management</button> <button class="tab ${this.activeTab === 'admin' ? 'active' : ''}" data-tab="admin">Admin</button> <button class="tab ${this.activeTab === 'errors' ? 'active' : ''}" data-tab="errors">Errors</button> </div> </div> <div class="events-list" id="logsList"> ${ this.error ? `<div class="error-message">${this.error}</div>` : this.isLoading ? '<div class="loading">Loading logs...</div>' : this.logs.length === 0 ? '<div class="no-events">No events found for the selected filter.</div>' : this.logs.map(log => this.renderEventItem(log)).join('') } </div> </div> </div> `; } /** * Setup event listeners */ setupEventListeners() { if (!this.container) return; // Tab switching const tabs = this.container.querySelectorAll('.tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { this.activeTab = tab.dataset.tab; this.updateTabDisplay(); }); }); // Time window control const timeWindowSelect = this.container.querySelector('#timeWindow'); if (timeWindowSelect) { timeWindowSelect.addEventListener('change', e => { this.timeWindow = parseInt(e.target.value); this.cleanupOldLogs(); }); } // Category filter const categoryFilter = this.container.querySelector('#categoryFilter'); if (categoryFilter) { categoryFilter.addEventListener('change', e => { this.kpiFilter.category = e.target.value; this.updateMetrics(); }); } // Function filter const functionFilter = this.container.querySelector('#functionFilter'); if (functionFilter) { functionFilter.addEventListener('change', e => { this.kpiFilter.function = e.target.value; this.updateMetrics(); }); } // Refresh button const refreshBtn = this.container.querySelector('#refreshLogs'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.fetchLogs(); }); } // Clear button const clearBtn = this.container.querySelector('#clearLogs'); if (clearBtn) { clearBtn.addEventListener('click', () => { this.logs = []; this.render(); }); } // Start monitoring button const startBtn = this.container.querySelector('#start-monitoring'); if (startBtn) { startBtn.addEventListener('click', async () => { await this.startPolling(); if (this.pollingInterval) { startBtn.style.display = 'none'; const stopBtn = this.container.querySelector('#stop-monitoring'); if (stopBtn) stopBtn.style.display = 'inline-block'; } }); } // Stop monitoring button const stopBtn = this.container.querySelector('#stop-monitoring'); if (stopBtn) { stopBtn.addEventListener('click', () => { this.stopPolling(); stopBtn.style.display = 'none'; const startBtn = this.container.querySelector('#start-monitoring'); if (startBtn) startBtn.style.display = 'inline-block'; }); } } /** * Start polling for logs */ async startPolling() { this.isConnecting = true; this.failedAttempts = 0; // Reset failed attempts when starting this.render(); // First check if live logging is available try { const availabilityUrl = `${this.apiBaseUrl}/live_logs?bot_id=${this.botId}&management_id=${this.managementId}&management_secret=${this.managementSecret}&check_availability=true`; const response = await fetch(availabilityUrl); if (response.status === 503) { const errorData = await response.json(); console.log('โŒ Live logging not available:', errorData.error); this.error = errorData.error; this.isConnecting = false; this.render(); return; } } catch (error) { console.log('โš ๏ธ Could not check availability, proceeding with polling'); } // If we get here, start polling this.pollingInterval = setInterval(() => { this.fetchLogs(); this.cleanupOldLogs(); }, 2000); // Poll every 2 seconds } /** * Stop polling */ stopPolling() { if (this.pollingInterval) { clearInterval(this.pollingInterval); this.pollingInterval = null; } this.isConnecting = false; this.isConnected = false; this.failedAttempts = 0; // Reset failed attempts when stopping this.render(); } /** * Fetch logs from the API */ async fetchLogs() { if (this.isLoading) return; console.log('๐Ÿ“ก LiveLogsTab.fetchLogs() called'); this.isLoading = true; this.isConnected = false; try { const url = `${this.apiBaseUrl}/live_logs?bot_id=${this.botId}&management_id=${this.managementId}&management_secret=${this.managementSecret}&duration=${this.filters.duration}`; console.log('๐ŸŒ Fetching from URL:', url); const response = await fetch(url); console.log('๐Ÿ“ฅ Response status:', response.status); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.text(); console.log('๐Ÿ“„ Response data length:', data.length); const lines = data.split('\n').filter(line => line.trim()); console.log('๐Ÿ“‹ Parsed lines count:', lines.length); const newLogs = []; for (const line of lines) { if (line.startsWith('data: ')) { try { const logData = JSON.parse(line.substring(6)); newLogs.push(logData); } catch (e) { console.warn('Failed to parse log data:', line); } } } console.log('๐Ÿ“Š New logs found:', newLogs.length); if (newLogs.length > 0) { this.logs.unshift(...newLogs); this.updateMetrics(); this.render(); } this.isConnected = true; this.isConnecting = false; this.error = null; console.log('โœ… LiveLogsTab fetch successful'); } catch (error) { console.error('โŒ Failed to fetch logs:', error); this.error = error.message; this.isConnected = false; this.isConnecting = false; // Increment failed attempts this.failedAttempts++; console.log( `โš ๏ธ Failed attempt ${this.failedAttempts}/${this.maxFailedAttempts}` ); // Stop polling after max failed attempts if (this.failedAttempts >= this.maxFailedAttempts) { console.log('๐Ÿ›‘ Max failed attempts reached, stopping polling'); this.stopPolling(); this.error = `Connection failed after ${this.maxFailedAttempts} attempts. Please check your configuration and try again.`; } } finally { this.isLoading = false; this.updateConnectionStatus(); } } /** * Update connection status display */ updateConnectionStatus() { if (!this.container) return; const statusElement = this.container.querySelector('.system-status span'); if (statusElement) { statusElement.textContent = this.isConnected ? 'Connected' : 'Disconnected'; } } /** * Update tab display */ updateTabDisplay() { if (!this.container) return; const tabs = this.container.querySelectorAll('.tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === this.activeTab); }); this.render(); } /** * Clean up old logs based on time window */ cleanupOldLogs() { const cutoffTime = Date.now() - this.timeWindow * 1000; const initialCount = this.logs.length; this.logs = this.logs.filter(log => { const logTime = new Date(log.timestamp).getTime(); return logTime > cutoffTime; }); const removedCount = initialCount - this.logs.length; if (removedCount > 0) { this.cleanupNotice = { show: true, count: removedCount }; setTimeout(() => { this.cleanupNotice = { show: false, count: 0 }; }, 3000); } this.updateMetrics(); } /** * Update metrics based on current logs */ updateMetrics() { const now = Date.now(); const cutoffTime = now - this.timeWindow * 1000; const recentLogs = this.logs.filter(log => { const logTime = new Date(log.timestamp).getTime(); return logTime > cutoffTime; }); // Reset metrics this.metrics = { totalOperations: 0, totalManagement: 0, averageLatency: 0, totalTokens: 0, errorCount: 0, successCount: 0, startTime: now, }; let totalLatency = 0; let latencyCount = 0; recentLogs.forEach(log => { // Count by type if (log.function_name && log.function_name.includes('_endpoint')) { this.metrics.totalOperations++; } else { this.metrics.totalManagement++; } // Count errors if (log.level === 'Error' || log.level === 'error') { this.metrics.errorCount++; } else { this.metrics.successCount++; } // Sum tokens if (log.tokens_used) { this.metrics.totalTokens += parseInt(log.tokens_used) || 0; } // Calculate latency if (log.duration) { totalLatency += parseFloat(log.duration); latencyCount++; } }); // Calculate average latency this.metrics.averageLatency = latencyCount > 0 ? totalLatency / latencyCount : 0; // Update filtered metrics this.updateFilteredMetrics(recentLogs); // Update display this.updateMetricsDisplay(); } /** * Update filtered metrics */ updateFilteredMetrics(logs) { this.filteredMetrics = { operations: { total: 0, averageLatency: 0, errorCount: 0, successCount: 0, totalTokens: 0, }, management: { total: 0, averageLatency: 0, errorCount: 0, successCount: 0, totalTokens: 0, }, byFunction: {}, }; const operationLogs = logs.filter( log => log.function_name && log.function_name.includes('_endpoint') ); const managementLogs = logs.filter( log => !log.function_name || !log.function_name.includes('_endpoint') ); // Process operation logs operationLogs.forEach(log => { this.filteredMetrics.operations.total++; if (log.level === 'Error' || log.level === 'error') { this.filteredMetrics.operations.errorCount++; } else { this.filteredMetrics.operations.successCount++; } if (log.tokens_used) { this.filteredMetrics.operations.totalTokens += parseInt(log.tokens_used) || 0; } }); // Process management logs managementLogs.forEach(log => { this.filteredMetrics.management.total++; if (log.level === 'Error' || log.level === 'error') { this.filteredMetrics.management.errorCount++; } else { this.filteredMetrics.management.successCount++; } if (log.tokens_used) { this.filteredMetrics.management.totalTokens += parseInt(log.tokens_used) || 0; } }); // Group by function logs.forEach(log => { const functionName = log.function_name || 'unknown'; if (!this.filteredMetrics.byFunction[functionName]) { this.filteredMetrics.byFunction[functionName] = { total: 0, averageLatency: 0, errorCount: 0, successCount: 0, totalTokens: 0, }; } this.filteredMetrics.byFunction[functionName].total++; if (log.level === 'Error' || log.level === 'error') { this.filteredMetrics.byFunction[functionName].errorCount++; } else { this.filteredMetrics.byFunction[functionName].successCount++; } if (log.tokens_used) { this.filteredMetrics.byFunction[functionName].totalTokens += parseInt(log.tokens_used) || 0; } }); } /** * Update metrics display */ updateMetricsDisplay() { if (!this.container) return; const totalOperationsEl = this.container.querySelector('#totalOperations'); const avgLatencyEl = this.container.querySelector('#avgLatency'); const successRateEl = this.container.querySelector('#successRate'); const totalTokensEl = this.container.querySelector('#totalTokens'); if (totalOperationsEl) { totalOperationsEl.textContent = this.metrics.totalOperations; } if (avgLatencyEl) { avgLatencyEl.textContent = `${this.metrics.averageLatency.toFixed(2)}ms`; } if (successRateEl) { successRateEl.textContent = `${this.calculateSuccessRate()}%`; } if (totalTokensEl) { totalTokensEl.textContent = this.metrics.totalTokens; } } /** * Calculate success rate */ calculateSuccessRate() { const total = this.metrics.successCount + this.metrics.errorCount; if (total === 0) return 100; return Math.round((this.metrics.successCount / total) * 100); } /** * Render events list */ renderEvents() { const filteredLogs = this.getFilteredLogs(); if (filteredLogs.length === 0) { return ` <div class="event-item"> <div class="event-content">No events found for the selected filter.</div> </div> `; } return filteredLogs.map(log => this.renderEventItem(log)).join(''); } /** * Get filtered logs based on active tab */ getFilteredLogs() { switch (this.activeTab) { case 'operations': return this.logs.filter( log => log.function_name && log.function_name.includes('_endpoint') ); case 'management': return this.logs.filter( log => !log.function_name || !log.function_name.includes('_endpoint') ); case 'admin': return this.logs.filter( log => log.function_name && (log.function_name.includes('create_token') || log.function_name.includes('delete_token') || log.function_name.includes('get_active_bots') || log.function_name.includes('get_conversation_')) ); case 'errors': return this.logs.filter( log => log.level === 'Error' || log.level === 'error' ); default: return this.logs; } } /** * Render individual event item */ renderEventItem(log) { const timestamp = new Date(log.timestamp); const age = this.getAgeString(timestamp); const functionName = this.functionNameMap[log.function_name] || log.function_name || 'Unknown'; const level = log.level || 'Info'; return ` <div class="event-item"> <div class="event-header"> <span class="event-type ${level.toLowerCase()}">${level}</span> <span class="event-time">${timestamp.toLocaleTimeString()}</span> </div> <div class="event-content"> <strong>${functionName}</strong> ${log.message ? `: ${log.message}` : ''} </div> <div class="event-meta"> <span class="meta-item"> <strong>Function:</strong> ${log.function_name || 'N/A'} </span> ${log.duration ? `<span class="meta-item"><strong>Duration:</strong> ${parseFloat(log.duration).toFixed(2)}ms</span>` : ''} ${log.tokens_used ? `<span class="meta-item"><strong>Tokens:</strong> ${log.tokens_used}</span>` : ''} <span class="meta-item"> <strong>Age:</strong> ${age} </span> </div> </div> `; } /** * Get age string for timestamp */ getAgeString(timestamp) { const now = new Date(); const diff = now - timestamp; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ago`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s ago`; } else { return `${seconds}s ago`; } } /** * Clean up resources */ destroy() { this.stopPolling(); this.container = null; } }