Files
GopherGate/static/js/pages/logs.js
2026-02-26 11:51:36 -05:00

567 lines
19 KiB
JavaScript

// 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 = `
<tr>
<td colspan="4" class="empty-table">
<i class="fas fa-search"></i>
<div>No logs found matching your filters</div>
</td>
</tr>
`;
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 `
<tr class="log-row ${levelClass}" data-log-id="${log.id}">
<td>${time}</td>
<td>
<span class="log-level-badge ${levelClass}">
<i class="fas fa-${levelIcon}"></i>
${log.level.toUpperCase()}
</span>
</td>
<td>${log.source}</td>
<td>
<div class="log-message">${log.message}</div>
${log.details ? `<div class="log-details">${log.details}</div>` : ''}
</td>
</tr>
`;
}).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 = `
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3 class="modal-title">Log Details</h3>
<button class="modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="log-detail-grid">
<div class="detail-item">
<span class="detail-label">Timestamp:</span>
<span class="detail-value">${new Date(log.timestamp).toLocaleString()}</span>
</div>
<div class="detail-item">
<span class="detail-label">Level:</span>
<span class="detail-value">
<span class="log-level-badge log-${log.level}">
<i class="fas fa-${this.getLevelIcon(log.level)}"></i>
${log.level.toUpperCase()}
</span>
</span>
</div>
<div class="detail-item">
<span class="detail-label">Source:</span>
<span class="detail-value">${log.source}</span>
</div>
<div class="detail-item full-width">
<span class="detail-label">Message:</span>
<div class="detail-value message-box">${log.message}</div>
</div>
${log.details ? `
<div class="detail-item full-width">
<span class="detail-label">Details:</span>
<div class="detail-value details-box">${log.details}</div>
</div>
` : ''}
<div class="detail-item full-width">
<span class="detail-label">Raw JSON:</span>
<pre class="detail-value json-box">${JSON.stringify(log, null, 2)}</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary close-modal">Close</button>
<button class="btn btn-primary copy-json" data-json='${JSON.stringify(log)}'>
<i class="fas fa-copy"></i> Copy JSON
</button>
</div>
</div>
`;
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;
}