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
JSX
/**
* 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;
}
spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
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;
}
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;
}
}