chore: initial clean commit
This commit is contained in:
1119
static/css/dashboard.css
Normal file
1119
static/css/dashboard.css
Normal file
File diff suppressed because it is too large
Load Diff
175
static/index.html
Normal file
175
static/index.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LLM Proxy Gateway - Admin Dashboard</title>
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4/build/global/luxon.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Login Screen -->
|
||||
<div id="login-screen" class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<i class="fas fa-robot login-icon"></i>
|
||||
<h1>LLM Proxy Gateway</h1>
|
||||
<p class="login-subtitle">Admin Dashboard</p>
|
||||
</div>
|
||||
<form id="login-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i> Username
|
||||
</label>
|
||||
<input type="text" id="username" name="username" placeholder="admin" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock"></i> Password
|
||||
</label>
|
||||
<input type="password" id="password" name="password" placeholder="••••••••" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="login-btn">
|
||||
<i class="fas fa-sign-in-alt"></i> Sign In
|
||||
</button>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<p>Default credentials: admin / admin123</p>
|
||||
</div>
|
||||
</form>
|
||||
<div id="login-error" class="error-message" style="display: none;">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>Invalid credentials. Please try again.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard -->
|
||||
<div id="dashboard" class="dashboard-container" style="display: none;">
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<i class="fas fa-robot"></i>
|
||||
<span>LLM Proxy</span>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-menu">
|
||||
<div class="menu-section">
|
||||
<h3 class="menu-title">MAIN</h3>
|
||||
<a href="#overview" class="menu-item active" data-page="overview">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a href="#analytics" class="menu-item" data-page="analytics">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>Analytics</span>
|
||||
</a>
|
||||
<a href="#costs" class="menu-item" data-page="costs">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
<span>Cost Management</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<h3 class="menu-title">MANAGEMENT</h3>
|
||||
<a href="#clients" class="menu-item" data-page="clients">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Client Management</span>
|
||||
</a>
|
||||
<a href="#providers" class="menu-item" data-page="providers">
|
||||
<i class="fas fa-server"></i>
|
||||
<span>Providers</span>
|
||||
</a>
|
||||
<a href="#monitoring" class="menu-item" data-page="monitoring">
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
<span>Real-time Monitoring</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<h3 class="menu-title">SYSTEM</h3>
|
||||
<a href="#settings" class="menu-item" data-page="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a href="#logs" class="menu-item" data-page="logs">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>System Logs</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-name">Administrator</span>
|
||||
<span class="user-role">Super Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="logout-btn" id="logout-btn">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Top Navigation -->
|
||||
<header class="top-nav">
|
||||
<div class="nav-left">
|
||||
<h1 class="page-title" id="page-title">Dashboard Overview</h1>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="nav-item">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<i class="fas fa-sync-alt" id="refresh-btn"></i>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<span id="current-time">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="page-content" id="page-content">
|
||||
<!-- Overview page will be loaded here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket Status Indicator -->
|
||||
<div class="ws-status" id="ws-status">
|
||||
<span class="ws-dot"></span>
|
||||
<span class="ws-text">Connecting...</span>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/auth.js"></script>
|
||||
<script src="/js/dashboard.js"></script>
|
||||
<script src="/js/websocket.js"></script>
|
||||
<script src="/js/charts.js"></script>
|
||||
<script src="/js/pages/overview.js"></script>
|
||||
<script src="/js/pages/analytics.js"></script>
|
||||
<script src="/js/pages/costs.js"></script>
|
||||
<script src="/js/pages/clients.js"></script>
|
||||
<script src="/js/pages/providers.js"></script>
|
||||
<script src="/js/pages/monitoring.js"></script>
|
||||
<script src="/js/pages/settings.js"></script>
|
||||
<script src="/js/pages/logs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
269
static/js/auth.js
Normal file
269
static/js/auth.js
Normal file
@@ -0,0 +1,269 @@
|
||||
// Authentication Module for LLM Proxy Dashboard
|
||||
|
||||
class AuthManager {
|
||||
constructor() {
|
||||
this.isAuthenticated = false;
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check for existing session
|
||||
const savedToken = localStorage.getItem('dashboard_token');
|
||||
const savedUser = localStorage.getItem('dashboard_user');
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
this.token = savedToken;
|
||||
this.user = JSON.parse(savedUser);
|
||||
this.isAuthenticated = true;
|
||||
this.showDashboard();
|
||||
} else {
|
||||
this.showLogin();
|
||||
}
|
||||
|
||||
// Setup login form
|
||||
this.setupLoginForm();
|
||||
this.setupLogout();
|
||||
}
|
||||
|
||||
setupLoginForm() {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (!loginForm) return;
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
await this.login(username, password);
|
||||
});
|
||||
}
|
||||
|
||||
setupLogout() {
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (!logoutBtn) return;
|
||||
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
this.logout();
|
||||
});
|
||||
}
|
||||
|
||||
async login(username, password) {
|
||||
const errorElement = document.getElementById('login-error');
|
||||
const loginBtn = document.querySelector('.login-btn');
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Authenticating...';
|
||||
loginBtn.disabled = true;
|
||||
|
||||
// Simple authentication - in production, this would call an API
|
||||
// For now, using mock authentication
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
// Successful login
|
||||
this.token = this.generateToken();
|
||||
this.user = {
|
||||
username: 'admin',
|
||||
name: 'Administrator',
|
||||
role: 'Super Admin',
|
||||
avatar: null
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('dashboard_token', this.token);
|
||||
localStorage.setItem('dashboard_user', JSON.stringify(this.user));
|
||||
|
||||
this.isAuthenticated = true;
|
||||
this.showDashboard();
|
||||
|
||||
// Show success message
|
||||
this.showToast('Successfully logged in!', 'success');
|
||||
} else {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
} catch (error) {
|
||||
// Show error
|
||||
errorElement.style.display = 'flex';
|
||||
errorElement.querySelector('span').textContent = error.message;
|
||||
|
||||
// Reset button
|
||||
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
|
||||
loginBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('dashboard_token');
|
||||
localStorage.removeItem('dashboard_user');
|
||||
|
||||
// Reset state
|
||||
this.isAuthenticated = false;
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
|
||||
// Show login screen
|
||||
this.showLogin();
|
||||
|
||||
// Show logout message
|
||||
this.showToast('Successfully logged out', 'info');
|
||||
}
|
||||
|
||||
generateToken() {
|
||||
// Generate a simple token for demo purposes
|
||||
// In production, this would come from the server
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
return btoa(`${timestamp}:${random}`).replace(/=/g, '');
|
||||
}
|
||||
|
||||
showLogin() {
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const dashboard = document.getElementById('dashboard');
|
||||
|
||||
if (loginScreen) loginScreen.style.display = 'flex';
|
||||
if (dashboard) dashboard.style.display = 'none';
|
||||
|
||||
// Clear form
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) loginForm.reset();
|
||||
|
||||
// Hide error
|
||||
const errorElement = document.getElementById('login-error');
|
||||
if (errorElement) errorElement.style.display = 'none';
|
||||
|
||||
// Reset button
|
||||
const loginBtn = document.querySelector('.login-btn');
|
||||
if (loginBtn) {
|
||||
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
|
||||
loginBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
showDashboard() {
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const dashboard = document.getElementById('dashboard');
|
||||
|
||||
if (loginScreen) loginScreen.style.display = 'none';
|
||||
if (dashboard) dashboard.style.display = 'flex';
|
||||
|
||||
// Update user info in sidebar
|
||||
this.updateUserInfo();
|
||||
|
||||
// Initialize dashboard components
|
||||
if (typeof window.initDashboard === 'function') {
|
||||
window.initDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
updateUserInfo() {
|
||||
const userNameElement = document.querySelector('.user-name');
|
||||
const userRoleElement = document.querySelector('.user-role');
|
||||
|
||||
if (userNameElement && this.user) {
|
||||
userNameElement.textContent = this.user.name;
|
||||
}
|
||||
|
||||
if (userRoleElement && this.user) {
|
||||
userRoleElement.textContent = this.user.role;
|
||||
}
|
||||
}
|
||||
|
||||
getAuthHeaders() {
|
||||
if (!this.token) return {};
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
async fetchWithAuth(url, options = {}) {
|
||||
const headers = this.getAuthHeaders();
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token expired or invalid
|
||||
this.logout();
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Create toast container if it doesn't exist
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Create toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
// Set icon based on type
|
||||
let icon = 'info-circle';
|
||||
switch (type) {
|
||||
case 'success':
|
||||
icon = 'check-circle';
|
||||
break;
|
||||
case 'error':
|
||||
icon = 'exclamation-circle';
|
||||
break;
|
||||
case 'warning':
|
||||
icon = 'exclamation-triangle';
|
||||
break;
|
||||
}
|
||||
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-${icon} toast-icon"></i>
|
||||
<div class="toast-content">
|
||||
<div class="toast-title">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
|
||||
<div class="toast-message">${message}</div>
|
||||
</div>
|
||||
<button class="toast-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add close functionality
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
toast.remove();
|
||||
});
|
||||
|
||||
// Add to container
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize auth manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.authManager = new AuthManager();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = AuthManager;
|
||||
}
|
||||
533
static/js/charts.js
Normal file
533
static/js/charts.js
Normal file
@@ -0,0 +1,533 @@
|
||||
// Chart.js Configuration and Helpers
|
||||
|
||||
class ChartManager {
|
||||
constructor() {
|
||||
this.charts = new Map();
|
||||
this.defaultColors = [
|
||||
'#3b82f6', // Blue
|
||||
'#10b981', // Green
|
||||
'#f59e0b', // Yellow
|
||||
'#ef4444', // Red
|
||||
'#8b5cf6', // Purple
|
||||
'#ec4899', // Pink
|
||||
'#06b6d4', // Cyan
|
||||
'#84cc16', // Lime
|
||||
'#f97316', // Orange
|
||||
'#6366f1', // Indigo
|
||||
];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Register Chart.js plugins if needed
|
||||
this.registerPlugins();
|
||||
}
|
||||
|
||||
registerPlugins() {
|
||||
// Register a plugin for tooltip background
|
||||
Chart.register({
|
||||
id: 'customTooltip',
|
||||
beforeDraw: (chart) => {
|
||||
if (chart.tooltip._active && chart.tooltip._active.length) {
|
||||
const ctx = chart.ctx;
|
||||
const activePoint = chart.tooltip._active[0];
|
||||
const x = activePoint.element.x;
|
||||
const topY = chart.scales.y.top;
|
||||
const bottomY = chart.scales.y.bottom;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.setLineDash([5, 5]);
|
||||
ctx.moveTo(x, topY);
|
||||
ctx.lineTo(x, bottomY);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChart(canvasId, config) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) {
|
||||
console.warn(`Canvas element #${canvasId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Destroy existing chart if it exists
|
||||
if (this.charts.has(canvasId)) {
|
||||
this.charts.get(canvasId).destroy();
|
||||
}
|
||||
|
||||
// Create new chart
|
||||
const ctx = canvas.getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
...config,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
titleColor: '#1e293b',
|
||||
bodyColor: '#1e293b',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6,
|
||||
padding: 12,
|
||||
boxPadding: 6,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toLocaleString();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'nearest'
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
callback: function(value) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...config.options
|
||||
}
|
||||
});
|
||||
|
||||
// Store chart reference
|
||||
this.charts.set(canvasId, chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
destroyChart(canvasId) {
|
||||
if (this.charts.has(canvasId)) {
|
||||
this.charts.get(canvasId).destroy();
|
||||
this.charts.delete(canvasId);
|
||||
}
|
||||
}
|
||||
|
||||
destroyAllCharts() {
|
||||
this.charts.forEach((chart, canvasId) => {
|
||||
chart.destroy();
|
||||
});
|
||||
this.charts.clear();
|
||||
}
|
||||
|
||||
// Chart templates
|
||||
createLineChart(canvasId, data, options = {}) {
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: data.datasets.map((dataset, index) => ({
|
||||
label: dataset.label,
|
||||
data: dataset.data,
|
||||
borderColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
||||
backgroundColor: dataset.fill ? this.hexToRgba(dataset.color || this.defaultColors[index % this.defaultColors.length], 0.1) : 'transparent',
|
||||
borderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
||||
pointBorderColor: '#ffffff',
|
||||
pointBorderWidth: 2,
|
||||
fill: dataset.fill || false,
|
||||
tension: 0.4,
|
||||
...dataset
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toLocaleString();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
};
|
||||
|
||||
return this.createChart(canvasId, config);
|
||||
}
|
||||
|
||||
createBarChart(canvasId, data, options = {}) {
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: data.datasets.map((dataset, index) => ({
|
||||
label: dataset.label,
|
||||
data: dataset.data,
|
||||
backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
||||
borderColor: dataset.borderColor || '#ffffff',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
borderSkipped: false,
|
||||
...dataset
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toLocaleString();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
};
|
||||
|
||||
return this.createChart(canvasId, config);
|
||||
}
|
||||
|
||||
createPieChart(canvasId, data, options = {}) {
|
||||
const config = {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
data: data.data || [],
|
||||
backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length),
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2,
|
||||
hoverOffset: 15
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.raw || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = Math.round((value / total) * 100);
|
||||
return `${label}: ${value.toLocaleString()} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
};
|
||||
|
||||
return this.createChart(canvasId, config);
|
||||
}
|
||||
|
||||
createDoughnutChart(canvasId, data, options = {}) {
|
||||
const config = {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
data: data.data || [],
|
||||
backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length),
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2,
|
||||
hoverOffset: 15
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
cutout: '60%',
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.raw || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = Math.round((value / total) * 100);
|
||||
return `${label}: ${value.toLocaleString()} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
};
|
||||
|
||||
return this.createChart(canvasId, config);
|
||||
}
|
||||
|
||||
createHorizontalBarChart(canvasId, data, options = {}) {
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: data.datasets.map((dataset, index) => ({
|
||||
label: dataset.label,
|
||||
data: dataset.data,
|
||||
backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
||||
borderColor: dataset.borderColor || '#ffffff',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
...dataset
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.x !== null) {
|
||||
label += context.parsed.x.toLocaleString();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
callback: function(value) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
};
|
||||
|
||||
return this.createChart(canvasId, config);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
hexToRgba(hex, alpha = 1) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
generateTimeLabels(hours = 24) {
|
||||
const now = luxon.DateTime.now();
|
||||
const labels = [];
|
||||
|
||||
for (let i = hours - 1; i >= 0; i--) {
|
||||
const time = now.minus({ hours: i });
|
||||
labels.push(time.toFormat('HH:00'));
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
generateDateLabels(days = 7) {
|
||||
const now = luxon.DateTime.now();
|
||||
const labels = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = now.minus({ days: i });
|
||||
labels.push(date.toFormat('MMM dd'));
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Demo data generators
|
||||
generateDemoTimeSeries(hours = 24, seriesCount = 1) {
|
||||
const labels = this.generateTimeLabels(hours);
|
||||
const datasets = [];
|
||||
|
||||
for (let i = 0; i < seriesCount; i++) {
|
||||
const data = [];
|
||||
let value = Math.floor(Math.random() * 100) + 50;
|
||||
|
||||
for (let j = 0; j < hours; j++) {
|
||||
// Add some randomness but keep trend
|
||||
value += Math.floor(Math.random() * 20) - 10;
|
||||
value = Math.max(10, value);
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
datasets.push({
|
||||
label: `Series ${i + 1}`,
|
||||
data: data,
|
||||
color: this.defaultColors[i % this.defaultColors.length]
|
||||
});
|
||||
}
|
||||
|
||||
return { labels, datasets };
|
||||
}
|
||||
|
||||
generateDemoBarData(labels, seriesCount = 1) {
|
||||
const datasets = [];
|
||||
|
||||
for (let i = 0; i < seriesCount; i++) {
|
||||
const data = labels.map(() => Math.floor(Math.random() * 100) + 20);
|
||||
|
||||
datasets.push({
|
||||
label: `Dataset ${i + 1}`,
|
||||
data: data,
|
||||
color: this.defaultColors[i % this.defaultColors.length]
|
||||
});
|
||||
}
|
||||
|
||||
return { labels, datasets };
|
||||
}
|
||||
|
||||
generateDemoPieData(labels) {
|
||||
const data = labels.map(() => Math.floor(Math.random() * 100) + 10);
|
||||
const total = data.reduce((a, b) => a + b, 0);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
data: data,
|
||||
colors: labels.map((_, i) => this.defaultColors[i % this.defaultColors.length])
|
||||
};
|
||||
}
|
||||
|
||||
// Update chart data
|
||||
updateChartData(canvasId, newData) {
|
||||
const chart = this.charts.get(canvasId);
|
||||
if (!chart) return;
|
||||
|
||||
chart.data.labels = newData.labels || chart.data.labels;
|
||||
|
||||
if (newData.datasets) {
|
||||
newData.datasets.forEach((dataset, index) => {
|
||||
if (chart.data.datasets[index]) {
|
||||
chart.data.datasets[index].data = dataset.data;
|
||||
if (dataset.label) {
|
||||
chart.data.datasets[index].label = dataset.label;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chart.update();
|
||||
}
|
||||
|
||||
// Add data point to time series
|
||||
addDataPoint(canvasId, newPoint, datasetIndex = 0) {
|
||||
const chart = this.charts.get(canvasId);
|
||||
if (!chart || chart.config.type !== 'line') return;
|
||||
|
||||
const dataset = chart.data.datasets[datasetIndex];
|
||||
if (!dataset) return;
|
||||
|
||||
// Add new point
|
||||
dataset.data.push(newPoint);
|
||||
|
||||
// Remove oldest point if we have too many
|
||||
if (dataset.data.length > 100) {
|
||||
dataset.data.shift();
|
||||
chart.data.labels.shift();
|
||||
} else {
|
||||
// Add new label
|
||||
const now = luxon.DateTime.now();
|
||||
chart.data.labels.push(now.toFormat('HH:mm:ss'));
|
||||
}
|
||||
|
||||
chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize chart manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.chartManager = new ChartManager();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ChartManager;
|
||||
}
|
||||
872
static/js/dashboard.js
Normal file
872
static/js/dashboard.js
Normal file
@@ -0,0 +1,872 @@
|
||||
// Main Dashboard Controller
|
||||
|
||||
class Dashboard {
|
||||
constructor() {
|
||||
this.currentPage = 'overview';
|
||||
this.pages = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize only if authenticated
|
||||
if (!window.authManager || !window.authManager.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupNavigation();
|
||||
this.setupSidebar();
|
||||
this.setupRefresh();
|
||||
this.updateTime();
|
||||
this.loadPage(this.currentPage);
|
||||
|
||||
// Start time updates
|
||||
setInterval(() => this.updateTime(), 1000);
|
||||
}
|
||||
|
||||
setupNavigation() {
|
||||
// Handle menu item clicks
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get page from data attribute or href
|
||||
const page = item.getAttribute('data-page') ||
|
||||
item.getAttribute('href').substring(1);
|
||||
|
||||
// Update active state
|
||||
menuItems.forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Load page
|
||||
this.loadPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle hash changes (browser back/forward)
|
||||
window.addEventListener('hashchange', () => {
|
||||
const page = window.location.hash.substring(1) || 'overview';
|
||||
this.loadPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
setupSidebar() {
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
|
||||
if (toggleBtn && sidebar) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
// Save preference
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
localStorage.setItem('sidebar_collapsed', isCollapsed);
|
||||
});
|
||||
|
||||
// Load saved preference
|
||||
const savedState = localStorage.getItem('sidebar_collapsed');
|
||||
if (savedState === 'true') {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupRefresh() {
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
this.refreshCurrentPage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (!timeElement) return;
|
||||
|
||||
const now = luxon.DateTime.now();
|
||||
timeElement.textContent = now.toFormat('HH:mm:ss');
|
||||
}
|
||||
|
||||
async loadPage(page) {
|
||||
// Update current page
|
||||
this.currentPage = page;
|
||||
|
||||
// Update URL hash
|
||||
window.location.hash = page;
|
||||
|
||||
// Update page title
|
||||
this.updatePageTitle(page);
|
||||
|
||||
// Show loading state
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
// Load page content
|
||||
await this.loadPageContent(page);
|
||||
|
||||
// Initialize page-specific functionality
|
||||
await this.initializePage(page);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error loading page ${page}:`, error);
|
||||
this.showError(`Failed to load ${page} page`);
|
||||
} finally {
|
||||
// Hide loading state
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
updatePageTitle(page) {
|
||||
const titleElement = document.getElementById('page-title');
|
||||
if (!titleElement) return;
|
||||
|
||||
const titles = {
|
||||
'overview': 'Dashboard Overview',
|
||||
'analytics': 'Usage Analytics',
|
||||
'costs': 'Cost Management',
|
||||
'clients': 'Client Management',
|
||||
'providers': 'Provider Configuration',
|
||||
'monitoring': 'Real-time Monitoring',
|
||||
'settings': 'System Settings',
|
||||
'logs': 'System Logs'
|
||||
};
|
||||
|
||||
titleElement.textContent = titles[page] || 'Dashboard';
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
|
||||
content.classList.add('loading');
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
|
||||
content.classList.remove('loading');
|
||||
}
|
||||
|
||||
async loadPageContent(page) {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
|
||||
// For now, we'll generate content dynamically
|
||||
// In a real app, you might fetch HTML templates or use a framework
|
||||
|
||||
let html = '';
|
||||
|
||||
switch (page) {
|
||||
case 'overview':
|
||||
html = await this.getOverviewContent();
|
||||
break;
|
||||
case 'analytics':
|
||||
html = await this.getAnalyticsContent();
|
||||
break;
|
||||
case 'costs':
|
||||
html = await this.getCostsContent();
|
||||
break;
|
||||
case 'clients':
|
||||
html = await this.getClientsContent();
|
||||
break;
|
||||
case 'providers':
|
||||
html = await this.getProvidersContent();
|
||||
break;
|
||||
case 'monitoring':
|
||||
html = await this.getMonitoringContent();
|
||||
break;
|
||||
case 'settings':
|
||||
html = await this.getSettingsContent();
|
||||
break;
|
||||
case 'logs':
|
||||
html = await this.getLogsContent();
|
||||
break;
|
||||
default:
|
||||
html = '<div class="empty-state"><h3>Page not found</h3></div>';
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
async initializePage(page) {
|
||||
// Initialize page-specific JavaScript
|
||||
switch (page) {
|
||||
case 'overview':
|
||||
if (typeof window.initOverview === 'function') {
|
||||
await window.initOverview();
|
||||
}
|
||||
break;
|
||||
case 'analytics':
|
||||
if (typeof window.initAnalytics === 'function') {
|
||||
await window.initAnalytics();
|
||||
}
|
||||
break;
|
||||
case 'costs':
|
||||
if (typeof window.initCosts === 'function') {
|
||||
await window.initCosts();
|
||||
}
|
||||
break;
|
||||
case 'clients':
|
||||
if (typeof window.initClients === 'function') {
|
||||
await window.initClients();
|
||||
}
|
||||
break;
|
||||
case 'providers':
|
||||
if (typeof window.initProviders === 'function') {
|
||||
await window.initProviders();
|
||||
}
|
||||
break;
|
||||
case 'monitoring':
|
||||
if (typeof window.initMonitoring === 'function') {
|
||||
await window.initMonitoring();
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
if (typeof window.initSettings === 'function') {
|
||||
await window.initSettings();
|
||||
}
|
||||
break;
|
||||
case 'logs':
|
||||
if (typeof window.initLogs === 'function') {
|
||||
await window.initLogs();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refreshCurrentPage() {
|
||||
this.loadPage(this.currentPage);
|
||||
|
||||
// Show refresh animation
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.classList.add('fa-spin');
|
||||
setTimeout(() => {
|
||||
refreshBtn.classList.remove('fa-spin');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Page refreshed', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<h3>Error</h3>
|
||||
<p>${message}</p>
|
||||
<button class="btn btn-primary" onclick="window.dashboard.refreshCurrentPage()">
|
||||
<i class="fas fa-redo"></i> Try Again
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Page content generators
|
||||
async getOverviewContent() {
|
||||
return `
|
||||
<div class="stats-grid" id="overview-stats">
|
||||
<!-- Stats will be loaded dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Request Volume (Last 24 Hours)</h3>
|
||||
<div class="chart-controls">
|
||||
<button class="chart-control-btn active" data-period="24h">24H</button>
|
||||
<button class="chart-control-btn" data-period="7d">7D</button>
|
||||
<button class="chart-control-btn" data-period="30d">30D</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="requests-chart" height="300"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Provider Distribution</h3>
|
||||
</div>
|
||||
<canvas id="providers-chart" height="250"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">System Health</h3>
|
||||
</div>
|
||||
<div id="system-health">
|
||||
<!-- Health indicators will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Recent Requests</h3>
|
||||
<p class="card-subtitle">Last 50 requests</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="card-action-btn" title="Refresh">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table" id="recent-requests">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Client</th>
|
||||
<th>Provider</th>
|
||||
<th>Model</th>
|
||||
<th>Tokens</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Requests will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getAnalyticsContent() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Usage Analytics</h3>
|
||||
<p class="card-subtitle">Filter and analyze usage data</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-secondary">
|
||||
<i class="fas fa-download"></i> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-control">
|
||||
<label>Date Range</label>
|
||||
<select id="date-range">
|
||||
<option value="24h">Last 24 Hours</option>
|
||||
<option value="7d" selected>Last 7 Days</option>
|
||||
<option value="30d">Last 30 Days</option>
|
||||
<option value="custom">Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Client</label>
|
||||
<select id="client-filter">
|
||||
<option value="all">All Clients</option>
|
||||
<!-- Client options will be loaded dynamically -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Provider</label>
|
||||
<select id="provider-filter">
|
||||
<option value="all">All Providers</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Request Trends</h3>
|
||||
<div class="chart-controls">
|
||||
<button class="chart-control-btn active" data-metric="requests">Requests</button>
|
||||
<button class="chart-control-btn" data-metric="tokens">Tokens</button>
|
||||
<button class="chart-control-btn" data-metric="cost">Cost</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="analytics-chart" height="350"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Top Clients</h3>
|
||||
</div>
|
||||
<canvas id="clients-chart" height="300"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Top Models</h3>
|
||||
</div>
|
||||
<canvas id="models-chart" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Detailed Usage Data</h3>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table" id="usage-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Client</th>
|
||||
<th>Provider</th>
|
||||
<th>Model</th>
|
||||
<th>Requests</th>
|
||||
<th>Tokens</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Usage data will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getCostsContent() {
|
||||
return `
|
||||
<div class="stats-grid" id="cost-stats">
|
||||
<!-- Cost stats will be loaded dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Cost Breakdown</h3>
|
||||
<div class="chart-controls">
|
||||
<button class="chart-control-btn active" data-breakdown="provider">By Provider</button>
|
||||
<button class="chart-control-btn" data-breakdown="client">By Client</button>
|
||||
<button class="chart-control-btn" data-breakdown="model">By Model</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="costs-chart" height="300"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Budget Tracking</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="budget-progress">
|
||||
<!-- Budget progress bars will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Cost Projections</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="cost-projections">
|
||||
<!-- Projections will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Pricing Configuration</h3>
|
||||
<p class="card-subtitle">Current provider pricing</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" id="edit-pricing">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table" id="pricing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Model</th>
|
||||
<th>Input Price</th>
|
||||
<th>Output Price</th>
|
||||
<th>Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Pricing data will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getClientsContent() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Client Management</h3>
|
||||
<p class="card-subtitle">Manage API clients and tokens</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" id="add-client">
|
||||
<i class="fas fa-plus"></i> Add Client
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table" id="clients-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Name</th>
|
||||
<th>Token</th>
|
||||
<th>Created</th>
|
||||
<th>Last Used</th>
|
||||
<th>Requests</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Clients will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Client Usage Summary</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="client-usage-chart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Rate Limit Status</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="rate-limit-status">
|
||||
<!-- Rate limit status will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getProvidersContent() {
|
||||
return `
|
||||
<div class="stats-grid" id="provider-stats">
|
||||
<!-- Provider stats will be loaded dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Provider Configuration</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="providers-list">
|
||||
<!-- Providers will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Model Availability</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="models-list">
|
||||
<!-- Models will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Connection Tests</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="connection-tests">
|
||||
<!-- Test results will be loaded dynamically -->
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" id="test-all-providers">
|
||||
<i class="fas fa-play"></i> Test All Providers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getMonitoringContent() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Real-time Monitoring</h3>
|
||||
<p class="card-subtitle">Live request stream and system metrics</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-secondary" id="pause-monitoring">
|
||||
<i class="fas fa-pause"></i> Pause
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<h4>Live Request Stream</h4>
|
||||
<div id="request-stream" class="monitoring-stream">
|
||||
<!-- Live requests will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>System Metrics</h4>
|
||||
<div id="system-metrics">
|
||||
<!-- System metrics will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-3">
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Response Time (ms)</h3>
|
||||
</div>
|
||||
<canvas id="response-time-chart" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Error Rate (%)</h3>
|
||||
</div>
|
||||
<canvas id="error-rate-chart" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Rate Limit Usage</h3>
|
||||
</div>
|
||||
<canvas id="rate-limit-chart" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">System Logs (Live)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="system-logs" class="log-stream">
|
||||
<!-- System logs will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getSettingsContent() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">System Settings</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="settings-form">
|
||||
<div class="form-section">
|
||||
<h4>General Configuration</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-control">
|
||||
<label>Server Port</label>
|
||||
<input type="number" id="server-port" value="8080" min="1024" max="65535">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Log Level</label>
|
||||
<select id="log-level">
|
||||
<option value="error">Error</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="info" selected>Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Database Settings</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-control">
|
||||
<label>Database Path</label>
|
||||
<input type="text" id="db-path" value="./data/llm-proxy.db">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Backup Interval (hours)</label>
|
||||
<input type="number" id="backup-interval" value="24" min="1" max="168">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Security Settings</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-control">
|
||||
<label>Dashboard Password</label>
|
||||
<input type="password" id="dashboard-password" placeholder="Leave empty to keep current">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Session Timeout (minutes)</label>
|
||||
<input type="number" id="session-timeout" value="30" min="5" max="1440">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="reset-settings">
|
||||
<i class="fas fa-undo"></i> Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Database Management</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-secondary" id="backup-db">
|
||||
<i class="fas fa-download"></i> Backup Database
|
||||
</button>
|
||||
<button class="btn btn-warning" id="optimize-db">
|
||||
<i class="fas fa-magic"></i> Optimize Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">System Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="system-info">
|
||||
<!-- System info will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async getLogsContent() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">System Logs</h3>
|
||||
<p class="card-subtitle">View and filter system logs</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-secondary" id="download-logs">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-danger" id="clear-logs">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-control">
|
||||
<label>Log Level</label>
|
||||
<select id="log-filter">
|
||||
<option value="all">All Levels</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Time Range</label>
|
||||
<select id="log-time-range">
|
||||
<option value="1h">Last Hour</option>
|
||||
<option value="24h" selected>Last 24 Hours</option>
|
||||
<option value="7d">Last 7 Days</option>
|
||||
<option value="30d">Last 30 Days</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Search</label>
|
||||
<input type="text" id="log-search" placeholder="Search logs...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table" id="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Level</th>
|
||||
<th>Source</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Logs will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.initDashboard = () => {
|
||||
window.dashboard = new Dashboard();
|
||||
};
|
||||
|
||||
// If already authenticated, initialize immediately
|
||||
if (window.authManager && window.authManager.isAuthenticated) {
|
||||
window.initDashboard();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Dashboard;
|
||||
}
|
||||
334
static/js/pages/analytics.js
Normal file
334
static/js/pages/analytics.js
Normal file
@@ -0,0 +1,334 @@
|
||||
// Analytics Page Module
|
||||
|
||||
class AnalyticsPage {
|
||||
constructor() {
|
||||
this.filters = {
|
||||
dateRange: '7d',
|
||||
client: 'all',
|
||||
provider: 'all'
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load initial data
|
||||
await this.loadFilters();
|
||||
await this.loadCharts();
|
||||
await this.loadUsageData();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadFilters() {
|
||||
try {
|
||||
// Load clients for filter dropdown
|
||||
// In a real app, this would fetch from /api/clients
|
||||
const clients = [
|
||||
{ id: 'client-1', name: 'Web Application' },
|
||||
{ id: 'client-2', name: 'Mobile App' },
|
||||
{ id: 'client-3', name: 'API Integration' },
|
||||
{ id: 'client-4', name: 'Internal Tools' },
|
||||
{ id: 'client-5', name: 'Testing Suite' }
|
||||
];
|
||||
|
||||
this.renderClientFilter(clients);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderClientFilter(clients) {
|
||||
const select = document.getElementById('client-filter');
|
||||
if (!select) return;
|
||||
|
||||
// Clear existing options except "All Clients"
|
||||
while (select.options.length > 1) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
// Add client options
|
||||
clients.forEach(client => {
|
||||
const option = document.createElement('option');
|
||||
option.value = client.id;
|
||||
option.textContent = client.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async loadCharts() {
|
||||
await this.loadAnalyticsChart();
|
||||
await this.loadClientsChart();
|
||||
await this.loadModelsChart();
|
||||
}
|
||||
|
||||
async loadAnalyticsChart() {
|
||||
try {
|
||||
// Generate demo data
|
||||
const labels = window.chartManager.generateDateLabels(7);
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Requests',
|
||||
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
|
||||
color: '#3b82f6',
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
|
||||
color: '#10b981',
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Cost ($)',
|
||||
data: labels.map(() => Math.random() * 50 + 10),
|
||||
color: '#f59e0b',
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Create chart
|
||||
window.chartManager.createLineChart('analytics-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading analytics chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadClientsChart() {
|
||||
try {
|
||||
const data = {
|
||||
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: [45, 25, 15, 10, 5],
|
||||
color: '#3b82f6'
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createHorizontalBarChart('clients-chart', data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading clients chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadModelsChart() {
|
||||
try {
|
||||
const data = {
|
||||
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'],
|
||||
data: [35, 30, 20, 10, 5],
|
||||
colors: ['#3b82f6', '#60a5fa', '#10b981', '#f59e0b', '#8b5cf6']
|
||||
};
|
||||
|
||||
window.chartManager.createDoughnutChart('models-chart', data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading models chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadUsageData() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/usage/detailed
|
||||
const usageData = [
|
||||
{ date: '2024-01-15', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 245, tokens: 125000, cost: 12.50 },
|
||||
{ date: '2024-01-15', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 180, tokens: 89000, cost: 8.90 },
|
||||
{ date: '2024-01-15', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 320, tokens: 156000, cost: 15.60 },
|
||||
{ date: '2024-01-14', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 210, tokens: 110000, cost: 11.00 },
|
||||
{ date: '2024-01-14', client: 'Internal Tools', provider: 'DeepSeek', model: 'deepseek-chat', requests: 95, tokens: 48000, cost: 4.80 },
|
||||
{ date: '2024-01-14', client: 'Testing Suite', provider: 'Grok', model: 'grok-beta', requests: 45, tokens: 22000, cost: 2.20 },
|
||||
{ date: '2024-01-13', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 195, tokens: 98000, cost: 9.80 },
|
||||
{ date: '2024-01-13', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 165, tokens: 82000, cost: 8.20 },
|
||||
{ date: '2024-01-13', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 285, tokens: 142000, cost: 14.20 },
|
||||
{ date: '2024-01-12', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 230, tokens: 118000, cost: 11.80 }
|
||||
];
|
||||
|
||||
this.renderUsageTable(usageData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading usage data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderUsageTable(data) {
|
||||
const tableBody = document.querySelector('#usage-table tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = data.map(row => `
|
||||
<tr>
|
||||
<td>${row.date}</td>
|
||||
<td>${row.client}</td>
|
||||
<td>${row.provider}</td>
|
||||
<td>${row.model}</td>
|
||||
<td>${row.requests.toLocaleString()}</td>
|
||||
<td>${row.tokens.toLocaleString()}</td>
|
||||
<td>$${row.cost.toFixed(2)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Filter controls
|
||||
const dateRangeSelect = document.getElementById('date-range');
|
||||
const clientSelect = document.getElementById('client-filter');
|
||||
const providerSelect = document.getElementById('provider-filter');
|
||||
|
||||
if (dateRangeSelect) {
|
||||
dateRangeSelect.addEventListener('change', (e) => {
|
||||
this.filters.dateRange = e.target.value;
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
if (clientSelect) {
|
||||
clientSelect.addEventListener('change', (e) => {
|
||||
this.filters.client = e.target.value;
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
if (providerSelect) {
|
||||
providerSelect.addEventListener('change', (e) => {
|
||||
this.filters.provider = e.target.value;
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Chart metric buttons
|
||||
const metricButtons = document.querySelectorAll('.chart-control-btn[data-metric]');
|
||||
metricButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Update active state
|
||||
metricButtons.forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
// Update chart based on metric
|
||||
this.updateAnalyticsChart(button.dataset.metric);
|
||||
});
|
||||
});
|
||||
|
||||
// Export button
|
||||
const exportBtn = document.querySelector('#analytics .btn-secondary');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', () => {
|
||||
this.exportData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
console.log('Applying filters:', this.filters);
|
||||
// In a real app, this would fetch filtered data from the API
|
||||
// For now, just show a toast
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Filters applied', 'success');
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
this.loadCharts();
|
||||
this.loadUsageData();
|
||||
}
|
||||
|
||||
updateAnalyticsChart(metric) {
|
||||
// Update the main analytics chart to show the selected metric
|
||||
const labels = window.chartManager.generateDateLabels(7);
|
||||
|
||||
let data;
|
||||
if (metric === 'requests') {
|
||||
data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
|
||||
color: '#3b82f6',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
} else if (metric === 'tokens') {
|
||||
data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Tokens',
|
||||
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
|
||||
color: '#10b981',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
} else if (metric === 'cost') {
|
||||
data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Cost ($)',
|
||||
data: labels.map(() => Math.random() * 50 + 10),
|
||||
color: '#f59e0b',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
window.chartManager.updateChartData('analytics-chart', data);
|
||||
}
|
||||
|
||||
exportData() {
|
||||
// Create CSV data
|
||||
const table = document.getElementById('usage-table');
|
||||
if (!table) return;
|
||||
|
||||
const rows = table.querySelectorAll('tr');
|
||||
const csv = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const rowData = [];
|
||||
row.querySelectorAll('th, td').forEach(cell => {
|
||||
rowData.push(`"${cell.textContent.replace(/"/g, '""')}"`);
|
||||
});
|
||||
csv.push(rowData.join(','));
|
||||
});
|
||||
|
||||
// Create download link
|
||||
const blob = new Blob([csv.join('\n')], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `llm-proxy-analytics-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Show success message
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Data exported successfully', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadCharts();
|
||||
this.loadUsageData();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize analytics page when needed
|
||||
window.initAnalytics = async () => {
|
||||
window.analyticsPage = new AnalyticsPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = AnalyticsPage;
|
||||
}
|
||||
471
static/js/pages/clients.js
Normal file
471
static/js/pages/clients.js
Normal file
@@ -0,0 +1,471 @@
|
||||
// Clients Page Module
|
||||
|
||||
class ClientsPage {
|
||||
constructor() {
|
||||
this.clients = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadClients();
|
||||
await this.loadClientUsageChart();
|
||||
await this.loadRateLimitStatus();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadClients() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/clients
|
||||
this.clients = [
|
||||
{ id: 'client-1', name: 'Web Application', token: 'sk-*****abc123', created: '2024-01-01', lastUsed: '2024-01-15', requests: 1245, status: 'active' },
|
||||
{ id: 'client-2', name: 'Mobile App', token: 'sk-*****def456', created: '2024-01-05', lastUsed: '2024-01-15', requests: 890, status: 'active' },
|
||||
{ id: 'client-3', name: 'API Integration', token: 'sk-*****ghi789', created: '2024-01-08', lastUsed: '2024-01-14', requests: 1560, status: 'active' },
|
||||
{ id: 'client-4', name: 'Internal Tools', token: 'sk-*****jkl012', created: '2024-01-10', lastUsed: '2024-01-13', requests: 340, status: 'inactive' },
|
||||
{ id: 'client-5', name: 'Testing Suite', token: 'sk-*****mno345', created: '2024-01-12', lastUsed: '2024-01-12', requests: 120, status: 'active' },
|
||||
{ id: 'client-6', name: 'Backup Service', token: 'sk-*****pqr678', created: '2024-01-14', lastUsed: null, requests: 0, status: 'pending' }
|
||||
];
|
||||
|
||||
this.renderClientsTable();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading clients:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderClientsTable() {
|
||||
const tableBody = document.querySelector('#clients-table tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = this.clients.map(client => {
|
||||
const statusClass = client.status === 'active' ? 'success' :
|
||||
client.status === 'inactive' ? 'warning' : 'secondary';
|
||||
const statusIcon = client.status === 'active' ? 'check-circle' :
|
||||
client.status === 'inactive' ? 'exclamation-triangle' : 'clock';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${client.id}</td>
|
||||
<td>${client.name}</td>
|
||||
<td>
|
||||
<code class="token-display">${client.token}</code>
|
||||
<button class="btn-copy-token" data-token="${client.token}" title="Copy token">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>${client.created}</td>
|
||||
<td>${client.lastUsed || 'Never'}</td>
|
||||
<td>${client.requests.toLocaleString()}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${client.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-action" title="Edit" data-action="edit" data-id="${client.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn-action" title="Rotate Token" data-action="rotate" data-id="${client.id}">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
<button class="btn-action danger" title="Revoke" data-action="revoke" data-id="${client.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for action buttons
|
||||
this.addActionStyles();
|
||||
}
|
||||
|
||||
addActionStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.token-display {
|
||||
background-color: var(--bg-secondary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-copy-token {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-copy-token:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-action.danger:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadClientUsageChart() {
|
||||
try {
|
||||
const data = {
|
||||
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: [1245, 890, 1560, 340, 120],
|
||||
color: '#3b82f6'
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createHorizontalBarChart('client-usage-chart', data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading client usage chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadRateLimitStatus() {
|
||||
const container = document.getElementById('rate-limit-status');
|
||||
if (!container) return;
|
||||
|
||||
const rateLimits = [
|
||||
{ client: 'Web Application', limit: 1000, used: 645, remaining: 355 },
|
||||
{ client: 'Mobile App', limit: 500, used: 320, remaining: 180 },
|
||||
{ client: 'API Integration', limit: 2000, used: 1560, remaining: 440 },
|
||||
{ client: 'Internal Tools', limit: 100, used: 34, remaining: 66 },
|
||||
{ client: 'Testing Suite', limit: 200, used: 120, remaining: 80 }
|
||||
];
|
||||
|
||||
container.innerHTML = rateLimits.map(limit => {
|
||||
const percentage = (limit.used / limit.limit) * 100;
|
||||
let color = 'success';
|
||||
if (percentage > 80) color = 'warning';
|
||||
if (percentage > 95) color = 'danger';
|
||||
|
||||
return `
|
||||
<div class="rate-limit-item">
|
||||
<div class="rate-limit-header">
|
||||
<span class="rate-limit-client">${limit.client}</span>
|
||||
<span class="rate-limit-numbers">${limit.used} / ${limit.limit}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${color}" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
<div class="rate-limit-footer">
|
||||
<span class="rate-limit-percentage">${Math.round(percentage)}% used</span>
|
||||
<span class="rate-limit-remaining">${limit.remaining} remaining</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for rate limit items
|
||||
this.addRateLimitStyles();
|
||||
}
|
||||
|
||||
addRateLimitStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.rate-limit-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rate-limit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rate-limit-client {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.rate-limit-numbers {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.rate-limit-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.rate-limit-percentage {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.rate-limit-remaining {
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Add client button
|
||||
const addBtn = document.getElementById('add-client');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', () => {
|
||||
this.showAddClientModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Copy token buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-copy-token')) {
|
||||
const button = e.target.closest('.btn-copy-token');
|
||||
const token = button.dataset.token;
|
||||
this.copyToClipboard(token);
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Token copied to clipboard', 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-action')) {
|
||||
const button = e.target.closest('.btn-action');
|
||||
const action = button.dataset.action;
|
||||
const clientId = button.dataset.id;
|
||||
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
this.editClient(clientId);
|
||||
break;
|
||||
case 'rotate':
|
||||
this.rotateToken(clientId);
|
||||
break;
|
||||
case 'revoke':
|
||||
this.revokeClient(clientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showAddClientModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Add New Client</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="add-client-form">
|
||||
<div class="form-control">
|
||||
<label for="client-name">Client Name</label>
|
||||
<input type="text" id="client-name" placeholder="e.g., Web Application" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="client-description">Description (Optional)</label>
|
||||
<textarea id="client-description" rows="3" placeholder="Describe what this client will be used for..."></textarea>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="rate-limit">Rate Limit (requests per hour)</label>
|
||||
<input type="number" id="rate-limit" value="1000" min="1" max="10000">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary create-client">Create Client</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const createBtn = modal.querySelector('.create-client');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
createBtn.addEventListener('click', () => {
|
||||
const name = modal.querySelector('#client-name').value;
|
||||
if (!name.trim()) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Client name is required', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real app, this would create the client via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Client "${name}" created successfully`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editClient(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show edit modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Edit Client: ${client.name}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Client editing would be implemented here.</p>
|
||||
<p>In a real implementation, this would include forms for updating client settings.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary save-client">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const saveBtn = modal.querySelector('.save-client');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// In a real app, this would save client changes
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Client updated successfully', 'success');
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rotateToken(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show confirmation modal
|
||||
if (confirm(`Are you sure you want to rotate the token for "${client.name}"? The old token will be invalidated.`)) {
|
||||
// In a real app, this would rotate the token via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Token rotated for "${client.name}"`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
}
|
||||
}
|
||||
|
||||
revokeClient(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show confirmation modal
|
||||
if (confirm(`Are you sure you want to revoke client "${client.name}"? This action cannot be undone.`)) {
|
||||
// In a real app, this would revoke the client via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Client "${client.name}" revoked`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
}
|
||||
}
|
||||
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadClients();
|
||||
this.loadClientUsageChart();
|
||||
this.loadRateLimitStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize clients page when needed
|
||||
window.initClients = async () => {
|
||||
window.clientsPage = new ClientsPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ClientsPage;
|
||||
}
|
||||
468
static/js/pages/costs.js
Normal file
468
static/js/pages/costs.js
Normal file
@@ -0,0 +1,468 @@
|
||||
// Costs Page Module
|
||||
|
||||
class CostsPage {
|
||||
constructor() {
|
||||
this.costData = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadCostStats();
|
||||
await this.loadCostsChart();
|
||||
await this.loadBudgetTracking();
|
||||
await this.loadCostProjections();
|
||||
await this.loadPricingTable();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadCostStats() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/costs/summary
|
||||
this.costData = {
|
||||
totalCost: 125.43,
|
||||
todayCost: 12.45,
|
||||
weekCost: 45.67,
|
||||
monthCost: 125.43,
|
||||
avgDailyCost: 8.36,
|
||||
costTrend: 5.2, // percentage
|
||||
budgetUsed: 62, // percentage
|
||||
projectedMonthEnd: 189.75
|
||||
};
|
||||
|
||||
this.renderCostStats();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading cost stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderCostStats() {
|
||||
const container = document.getElementById('cost-stats');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">$${this.costData.totalCost.toFixed(2)}</div>
|
||||
<div class="stat-label">Total Cost</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
$${this.costData.todayCost.toFixed(2)} today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-calendar-week"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">$${this.costData.weekCost.toFixed(2)}</div>
|
||||
<div class="stat-label">This Week</div>
|
||||
<div class="stat-change ${this.costData.costTrend > 0 ? 'positive' : 'negative'}">
|
||||
<i class="fas fa-arrow-${this.costData.costTrend > 0 ? 'up' : 'down'}"></i>
|
||||
${Math.abs(this.costData.costTrend)}% from last week
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">$${this.costData.monthCost.toFixed(2)}</div>
|
||||
<div class="stat-label">This Month</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
$${this.costData.avgDailyCost.toFixed(2)}/day avg
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-piggy-bank"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.costData.budgetUsed}%</div>
|
||||
<div class="stat-label">Budget Used</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
$${this.costData.projectedMonthEnd.toFixed(2)} projected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async loadCostsChart() {
|
||||
try {
|
||||
// Generate demo data
|
||||
const data = {
|
||||
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
|
||||
datasets: [{
|
||||
label: 'Cost by Provider',
|
||||
data: [65, 25, 8, 2],
|
||||
color: '#3b82f6'
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createBarChart('costs-chart', data, {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `$${context.parsed.y.toFixed(2)} (${context.parsed.y}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading costs chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadBudgetTracking() {
|
||||
const container = document.getElementById('budget-progress');
|
||||
if (!container) return;
|
||||
|
||||
const budgets = [
|
||||
{ name: 'Monthly Budget', used: 62, total: 200, color: 'primary' },
|
||||
{ name: 'OpenAI Budget', used: 75, total: 150, color: 'info' },
|
||||
{ name: 'Gemini Budget', used: 45, total: 100, color: 'success' },
|
||||
{ name: 'Team Budget', used: 30, total: 50, color: 'warning' }
|
||||
];
|
||||
|
||||
container.innerHTML = budgets.map(budget => `
|
||||
<div class="budget-item">
|
||||
<div class="budget-header">
|
||||
<span class="budget-name">${budget.name}</span>
|
||||
<span class="budget-amount">$${budget.used} / $${budget.total}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${budget.color}" style="width: ${(budget.used / budget.total) * 100}%"></div>
|
||||
</div>
|
||||
<div class="budget-footer">
|
||||
<span class="budget-percentage">${Math.round((budget.used / budget.total) * 100)}% used</span>
|
||||
<span class="budget-remaining">$${budget.total - budget.used} remaining</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for budget items
|
||||
this.addBudgetStyles();
|
||||
}
|
||||
|
||||
addBudgetStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.budget-item {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.budget-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.budget-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.budget-amount {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.budget-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.budget-percentage {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.budget-remaining {
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-fill.primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.progress-fill.info {
|
||||
background-color: var(--info);
|
||||
}
|
||||
|
||||
.progress-fill.success {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.progress-fill.warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadCostProjections() {
|
||||
const container = document.getElementById('cost-projections');
|
||||
if (!container) return;
|
||||
|
||||
const projections = [
|
||||
{ period: 'Today', amount: 12.45, trend: 'up' },
|
||||
{ period: 'This Week', amount: 45.67, trend: 'up' },
|
||||
{ period: 'This Month', amount: 189.75, trend: 'up' },
|
||||
{ period: 'Next Month', amount: 210.50, trend: 'up' }
|
||||
];
|
||||
|
||||
container.innerHTML = projections.map(proj => `
|
||||
<div class="projection-item">
|
||||
<div class="projection-period">${proj.period}</div>
|
||||
<div class="projection-amount">$${proj.amount.toFixed(2)}</div>
|
||||
<div class="projection-trend ${proj.trend}">
|
||||
<i class="fas fa-arrow-${proj.trend}"></i>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for projections
|
||||
this.addProjectionStyles();
|
||||
}
|
||||
|
||||
addProjectionStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.projection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.projection-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.projection-period {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.projection-amount {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.projection-trend {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.projection-trend.up {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.projection-trend.down {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadPricingTable() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/pricing
|
||||
const pricingData = [
|
||||
{ provider: 'OpenAI', model: 'gpt-4', input: 0.03, output: 0.06, updated: '2024-01-15' },
|
||||
{ provider: 'OpenAI', model: 'gpt-3.5-turbo', input: 0.0015, output: 0.002, updated: '2024-01-15' },
|
||||
{ provider: 'Gemini', model: 'gemini-pro', input: 0.0005, output: 0.0015, updated: '2024-01-14' },
|
||||
{ provider: 'Gemini', model: 'gemini-pro-vision', input: 0.0025, output: 0.0075, updated: '2024-01-14' },
|
||||
{ provider: 'DeepSeek', model: 'deepseek-chat', input: 0.00014, output: 0.00028, updated: '2024-01-13' },
|
||||
{ provider: 'DeepSeek', model: 'deepseek-coder', input: 0.00014, output: 0.00028, updated: '2024-01-13' },
|
||||
{ provider: 'Grok', model: 'grok-beta', input: 0.01, output: 0.03, updated: '2024-01-12' }
|
||||
];
|
||||
|
||||
this.renderPricingTable(pricingData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading pricing data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderPricingTable(data) {
|
||||
const tableBody = document.querySelector('#pricing-table tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = data.map(row => `
|
||||
<tr>
|
||||
<td>${row.provider}</td>
|
||||
<td>${row.model}</td>
|
||||
<td>$${row.input.toFixed(5)}/1K tokens</td>
|
||||
<td>$${row.output.toFixed(5)}/1K tokens</td>
|
||||
<td>${row.updated}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Breakdown buttons
|
||||
const breakdownButtons = document.querySelectorAll('.chart-control-btn[data-breakdown]');
|
||||
breakdownButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Update active state
|
||||
breakdownButtons.forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
// Update chart based on breakdown
|
||||
this.updateCostsChart(button.dataset.breakdown);
|
||||
});
|
||||
});
|
||||
|
||||
// Edit pricing button
|
||||
const editBtn = document.getElementById('edit-pricing');
|
||||
if (editBtn) {
|
||||
editBtn.addEventListener('click', () => {
|
||||
this.editPricing();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateCostsChart(breakdown) {
|
||||
let data;
|
||||
|
||||
if (breakdown === 'provider') {
|
||||
data = {
|
||||
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
|
||||
datasets: [{
|
||||
label: 'Cost by Provider',
|
||||
data: [65, 25, 8, 2],
|
||||
color: '#3b82f6'
|
||||
}]
|
||||
};
|
||||
} else if (breakdown === 'client') {
|
||||
data = {
|
||||
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
|
||||
datasets: [{
|
||||
label: 'Cost by Client',
|
||||
data: [40, 25, 20, 10, 5],
|
||||
color: '#10b981'
|
||||
}]
|
||||
};
|
||||
} else if (breakdown === 'model') {
|
||||
data = {
|
||||
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'],
|
||||
datasets: [{
|
||||
label: 'Cost by Model',
|
||||
data: [35, 30, 20, 10, 5],
|
||||
color: '#f59e0b'
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
window.chartManager.updateChartData('costs-chart', data);
|
||||
}
|
||||
|
||||
editPricing() {
|
||||
// Show pricing edit modal
|
||||
this.showPricingModal();
|
||||
}
|
||||
|
||||
showPricingModal() {
|
||||
// Create modal for editing pricing
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Edit Pricing Configuration</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Pricing configuration would be editable here.</p>
|
||||
<p>In a real implementation, this would include forms for updating provider pricing.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary save-pricing">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const saveBtn = modal.querySelector('.save-pricing');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// In a real app, this would save pricing changes
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Pricing updated successfully', 'success');
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadCostStats();
|
||||
this.loadCostsChart();
|
||||
this.loadBudgetTracking();
|
||||
this.loadCostProjections();
|
||||
this.loadPricingTable();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize costs page when needed
|
||||
window.initCosts = async () => {
|
||||
window.costsPage = new CostsPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CostsPage;
|
||||
}
|
||||
567
static/js/pages/logs.js
Normal file
567
static/js/pages/logs.js
Normal file
@@ -0,0 +1,567 @@
|
||||
// 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;
|
||||
}
|
||||
611
static/js/pages/monitoring.js
Normal file
611
static/js/pages/monitoring.js
Normal file
@@ -0,0 +1,611 @@
|
||||
// Monitoring Page Module
|
||||
|
||||
class MonitoringPage {
|
||||
constructor() {
|
||||
this.isPaused = false;
|
||||
this.requestStream = [];
|
||||
this.systemLogs = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load initial data
|
||||
await this.loadSystemMetrics();
|
||||
await this.loadCharts();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Setup WebSocket subscriptions
|
||||
this.setupWebSocketSubscriptions();
|
||||
|
||||
// Start simulated updates for demo
|
||||
this.startDemoUpdates();
|
||||
}
|
||||
|
||||
async loadSystemMetrics() {
|
||||
const container = document.getElementById('system-metrics');
|
||||
if (!container) return;
|
||||
|
||||
const metrics = [
|
||||
{ label: 'CPU Usage', value: '24%', trend: 'down', color: 'success' },
|
||||
{ label: 'Memory Usage', value: '1.8 GB', trend: 'stable', color: 'warning' },
|
||||
{ label: 'Disk I/O', value: '45 MB/s', trend: 'up', color: 'primary' },
|
||||
{ label: 'Network', value: '125 KB/s', trend: 'up', color: 'info' },
|
||||
{ label: 'Active Connections', value: '42', trend: 'stable', color: 'success' },
|
||||
{ label: 'Queue Length', value: '3', trend: 'down', color: 'success' }
|
||||
];
|
||||
|
||||
container.innerHTML = metrics.map(metric => `
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">${metric.label}</div>
|
||||
<div class="metric-value">${metric.value}</div>
|
||||
<div class="metric-trend ${metric.trend}">
|
||||
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for metrics
|
||||
this.addMetricStyles();
|
||||
}
|
||||
|
||||
addMetricStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.metric-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.metric-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-trend.up {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.metric-trend.down {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.metric-trend.stable {
|
||||
background-color: rgba(100, 116, 139, 0.1);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.monitoring-stream {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.stream-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.stream-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stream-entry.highlight {
|
||||
background-color: rgba(37, 99, 235, 0.05);
|
||||
}
|
||||
|
||||
.stream-entry-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.stream-entry-icon {
|
||||
font-size: 0.875rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stream-entry-content {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.stream-entry-details {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.log-stream {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-light);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-info .log-level {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.log-warn .log-level {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.log-error .log-level {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.log-debug .log-level {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadCharts() {
|
||||
await this.loadResponseTimeChart();
|
||||
await this.loadErrorRateChart();
|
||||
await this.loadRateLimitChart();
|
||||
}
|
||||
|
||||
async loadResponseTimeChart() {
|
||||
try {
|
||||
// Generate demo data for response time
|
||||
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Response Time (ms)',
|
||||
data: labels.map(() => Math.floor(Math.random() * 200) + 300),
|
||||
color: '#3b82f6',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createLineChart('response-time-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Milliseconds'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading response time chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadErrorRateChart() {
|
||||
try {
|
||||
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Error Rate (%)',
|
||||
data: labels.map(() => Math.random() * 5),
|
||||
color: '#ef4444',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createLineChart('error-rate-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Percentage'
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading error rate chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadRateLimitChart() {
|
||||
try {
|
||||
const labels = ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'];
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Rate Limit Usage',
|
||||
data: [65, 45, 78, 34, 60],
|
||||
color: '#10b981'
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createBarChart('rate-limit-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Percentage'
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading rate limit chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Pause/resume monitoring button
|
||||
const pauseBtn = document.getElementById('pause-monitoring');
|
||||
if (pauseBtn) {
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
this.togglePause();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupWebSocketSubscriptions() {
|
||||
if (!window.wsManager) return;
|
||||
|
||||
// Subscribe to request updates
|
||||
window.wsManager.subscribe('requests', (request) => {
|
||||
if (!this.isPaused) {
|
||||
this.addToRequestStream(request);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to log updates
|
||||
window.wsManager.subscribe('logs', (log) => {
|
||||
if (!this.isPaused) {
|
||||
this.addToLogStream(log);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to metric updates
|
||||
window.wsManager.subscribe('metrics', (metric) => {
|
||||
if (!this.isPaused) {
|
||||
this.updateCharts(metric);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
this.isPaused = !this.isPaused;
|
||||
const pauseBtn = document.getElementById('pause-monitoring');
|
||||
|
||||
if (pauseBtn) {
|
||||
if (this.isPaused) {
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
pauseBtn.classList.remove('btn-secondary');
|
||||
pauseBtn.classList.add('btn-success');
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Monitoring paused', 'warning');
|
||||
}
|
||||
} else {
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
pauseBtn.classList.remove('btn-success');
|
||||
pauseBtn.classList.add('btn-secondary');
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Monitoring resumed', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addToRequestStream(request) {
|
||||
const streamElement = document.getElementById('request-stream');
|
||||
if (!streamElement) return;
|
||||
|
||||
// Create entry
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'stream-entry';
|
||||
|
||||
// Format time
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// Determine icon based on status
|
||||
let icon = 'question-circle';
|
||||
let color = 'var(--text-secondary)';
|
||||
|
||||
if (request.status === 'success') {
|
||||
icon = 'check-circle';
|
||||
color = 'var(--success)';
|
||||
} else if (request.status === 'error') {
|
||||
icon = 'exclamation-circle';
|
||||
color = 'var(--danger)';
|
||||
}
|
||||
|
||||
entry.innerHTML = `
|
||||
<div class="stream-entry-time">${time}</div>
|
||||
<div class="stream-entry-icon" style="color: ${color}">
|
||||
<i class="fas fa-${icon}"></i>
|
||||
</div>
|
||||
<div class="stream-entry-content">
|
||||
<strong>${request.client_id || 'Unknown'}</strong> →
|
||||
${request.provider || 'Unknown'} (${request.model || 'Unknown'})
|
||||
<div class="stream-entry-details">
|
||||
${request.tokens || 0} tokens • ${request.duration || 0}ms
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to top of stream
|
||||
streamElement.insertBefore(entry, streamElement.firstChild);
|
||||
|
||||
// Store in memory (limit to 100)
|
||||
this.requestStream.unshift({
|
||||
time,
|
||||
request,
|
||||
element: entry
|
||||
});
|
||||
|
||||
if (this.requestStream.length > 100) {
|
||||
const oldEntry = this.requestStream.pop();
|
||||
if (oldEntry.element.parentNode) {
|
||||
oldEntry.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add highlight animation
|
||||
entry.classList.add('highlight');
|
||||
setTimeout(() => entry.classList.remove('highlight'), 1000);
|
||||
}
|
||||
|
||||
addToLogStream(log) {
|
||||
const logElement = document.getElementById('system-logs');
|
||||
if (!logElement) return;
|
||||
|
||||
// Create entry
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry log-${log.level || 'info'}`;
|
||||
|
||||
// Format time
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// Determine icon based on level
|
||||
let icon = 'info-circle';
|
||||
if (log.level === 'error') icon = 'exclamation-circle';
|
||||
if (log.level === 'warn') icon = 'exclamation-triangle';
|
||||
if (log.level === 'debug') icon = 'bug';
|
||||
|
||||
entry.innerHTML = `
|
||||
<div class="log-time">${time}</div>
|
||||
<div class="log-level">
|
||||
<i class="fas fa-${icon}"></i>
|
||||
</div>
|
||||
<div class="log-message">${log.message || ''}</div>
|
||||
`;
|
||||
|
||||
// Add to top of stream
|
||||
logElement.insertBefore(entry, logElement.firstChild);
|
||||
|
||||
// Store in memory (limit to 100)
|
||||
this.systemLogs.unshift({
|
||||
time,
|
||||
log,
|
||||
element: entry
|
||||
});
|
||||
|
||||
if (this.systemLogs.length > 100) {
|
||||
const oldEntry = this.systemLogs.pop();
|
||||
if (oldEntry.element.parentNode) {
|
||||
oldEntry.element.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCharts(metric) {
|
||||
// Update charts with new metric data
|
||||
if (metric.type === 'response_time' && window.chartManager.charts.has('response-time-chart')) {
|
||||
this.updateResponseTimeChart(metric.value);
|
||||
}
|
||||
|
||||
if (metric.type === 'error_rate' && window.chartManager.charts.has('error-rate-chart')) {
|
||||
this.updateErrorRateChart(metric.value);
|
||||
}
|
||||
}
|
||||
|
||||
updateResponseTimeChart(value) {
|
||||
window.chartManager.addDataPoint('response-time-chart', value);
|
||||
}
|
||||
|
||||
updateErrorRateChart(value) {
|
||||
window.chartManager.addDataPoint('error-rate-chart', value);
|
||||
}
|
||||
|
||||
startDemoUpdates() {
|
||||
// Simulate incoming requests for demo purposes
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
setInterval(() => {
|
||||
if (!this.isPaused && Math.random() > 0.3) { // 70% chance
|
||||
this.simulateRequest();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Simulate logs
|
||||
setInterval(() => {
|
||||
if (!this.isPaused && Math.random() > 0.5) { // 50% chance
|
||||
this.simulateLog();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Simulate metrics
|
||||
setInterval(() => {
|
||||
if (!this.isPaused) {
|
||||
this.simulateMetric();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
simulateRequest() {
|
||||
const clients = ['client-1', 'client-2', 'client-3', 'client-4', 'client-5'];
|
||||
const providers = ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'];
|
||||
const models = ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'];
|
||||
const statuses = ['success', 'success', 'success', 'error', 'warning']; // Mostly success
|
||||
|
||||
const request = {
|
||||
client_id: clients[Math.floor(Math.random() * clients.length)],
|
||||
provider: providers[Math.floor(Math.random() * providers.length)],
|
||||
model: models[Math.floor(Math.random() * models.length)],
|
||||
tokens: Math.floor(Math.random() * 2000) + 100,
|
||||
duration: Math.floor(Math.random() * 1000) + 100,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.addToRequestStream(request);
|
||||
}
|
||||
|
||||
simulateLog() {
|
||||
const levels = ['info', 'info', 'info', 'warn', 'error'];
|
||||
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'
|
||||
];
|
||||
|
||||
const log = {
|
||||
level: levels[Math.floor(Math.random() * levels.length)],
|
||||
message: messages[Math.floor(Math.random() * messages.length)],
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.addToLogStream(log);
|
||||
}
|
||||
|
||||
simulateMetric() {
|
||||
const metricTypes = ['response_time', 'error_rate'];
|
||||
const type = metricTypes[Math.floor(Math.random() * metricTypes.length)];
|
||||
|
||||
let value;
|
||||
if (type === 'response_time') {
|
||||
value = Math.floor(Math.random() * 200) + 300; // 300-500ms
|
||||
} else {
|
||||
value = Math.random() * 5; // 0-5%
|
||||
}
|
||||
|
||||
this.updateCharts({ type, value });
|
||||
}
|
||||
|
||||
clearStreams() {
|
||||
const streamElement = document.getElementById('request-stream');
|
||||
const logElement = document.getElementById('system-logs');
|
||||
|
||||
if (streamElement) {
|
||||
streamElement.innerHTML = '';
|
||||
this.requestStream = [];
|
||||
}
|
||||
|
||||
if (logElement) {
|
||||
logElement.innerHTML = '';
|
||||
this.systemLogs = [];
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadSystemMetrics();
|
||||
this.loadCharts();
|
||||
this.clearStreams();
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Monitoring refreshed', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize monitoring page when needed
|
||||
window.initMonitoring = async () => {
|
||||
window.monitoringPage = new MonitoringPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = MonitoringPage;
|
||||
}
|
||||
513
static/js/pages/overview.js
Normal file
513
static/js/pages/overview.js
Normal file
@@ -0,0 +1,513 @@
|
||||
// Overview Page Module
|
||||
|
||||
class OverviewPage {
|
||||
constructor() {
|
||||
this.stats = null;
|
||||
this.charts = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadStats();
|
||||
await this.loadCharts();
|
||||
await this.loadRecentRequests();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Subscribe to WebSocket updates
|
||||
this.setupWebSocketSubscriptions();
|
||||
}
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/usage/summary
|
||||
// For now, use mock data
|
||||
this.stats = {
|
||||
totalRequests: 12458,
|
||||
totalTokens: 1254300,
|
||||
totalCost: 125.43,
|
||||
activeClients: 8,
|
||||
errorRate: 2.3,
|
||||
avgResponseTime: 450,
|
||||
todayRequests: 342,
|
||||
todayCost: 12.45
|
||||
};
|
||||
|
||||
this.renderStats();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
this.showError('Failed to load statistics');
|
||||
}
|
||||
}
|
||||
|
||||
renderStats() {
|
||||
const container = document.getElementById('overview-stats');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.stats.totalRequests.toLocaleString()}</div>
|
||||
<div class="stat-label">Total Requests</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
${this.stats.todayRequests} today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-coins"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.stats.totalTokens.toLocaleString()}</div>
|
||||
<div class="stat-label">Total Tokens</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
12% from yesterday
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">$${this.stats.totalCost.toFixed(2)}</div>
|
||||
<div class="stat-label">Total Cost</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
$${this.stats.todayCost.toFixed(2)} today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.stats.activeClients}</div>
|
||||
<div class="stat-label">Active Clients</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
2 new this week
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.stats.errorRate}%</div>
|
||||
<div class="stat-label">Error Rate</div>
|
||||
<div class="stat-change negative">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
0.5% improvement
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.stats.avgResponseTime}ms</div>
|
||||
<div class="stat-label">Avg Response Time</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
50ms faster
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async loadCharts() {
|
||||
await this.loadRequestsChart();
|
||||
await this.loadProvidersChart();
|
||||
await this.loadSystemHealth();
|
||||
}
|
||||
|
||||
async loadRequestsChart() {
|
||||
try {
|
||||
// Generate demo data for requests chart
|
||||
const data = window.chartManager.generateDemoTimeSeries(24, 1);
|
||||
data.datasets[0].label = 'Requests per hour';
|
||||
data.datasets[0].fill = true;
|
||||
|
||||
// Create chart
|
||||
this.charts.requests = window.chartManager.createLineChart('requests-chart', data, {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `Requests: ${context.parsed.y}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading requests chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadProvidersChart() {
|
||||
try {
|
||||
const data = {
|
||||
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
|
||||
data: [45, 25, 20, 10],
|
||||
colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6']
|
||||
};
|
||||
|
||||
this.charts.providers = window.chartManager.createDoughnutChart('providers-chart', data, {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.raw || 0;
|
||||
return `${label}: ${value}% of requests`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading providers chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSystemHealth() {
|
||||
const container = document.getElementById('system-health');
|
||||
if (!container) return;
|
||||
|
||||
const healthData = [
|
||||
{ label: 'API Server', status: 'online', value: 100 },
|
||||
{ label: 'Database', status: 'online', value: 95 },
|
||||
{ label: 'OpenAI', status: 'online', value: 100 },
|
||||
{ label: 'Gemini', status: 'online', value: 100 },
|
||||
{ label: 'DeepSeek', status: 'warning', value: 85 },
|
||||
{ label: 'Grok', status: 'offline', value: 0 }
|
||||
];
|
||||
|
||||
container.innerHTML = healthData.map(item => `
|
||||
<div class="health-item">
|
||||
<div class="health-label">
|
||||
<span class="health-status status-badge ${item.status}">
|
||||
<i class="fas fa-circle"></i>
|
||||
${item.status}
|
||||
</span>
|
||||
<span class="health-name">${item.label}</span>
|
||||
</div>
|
||||
<div class="health-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${item.status}" style="width: ${item.value}%"></div>
|
||||
</div>
|
||||
<span class="health-value">${item.value}%</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for progress bars
|
||||
this.addHealthStyles();
|
||||
}
|
||||
|
||||
addHealthStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.health-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.health-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.health-name {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.health-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-fill.online {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.progress-fill.warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
|
||||
.progress-fill.offline {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
|
||||
.health-value {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadRecentRequests() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/requests/recent
|
||||
// For now, use mock data
|
||||
const requests = [
|
||||
{ time: '14:32:15', client: 'client-1', provider: 'OpenAI', model: 'gpt-4', tokens: 1250, status: 'success' },
|
||||
{ time: '14:30:45', client: 'client-2', provider: 'Gemini', model: 'gemini-pro', tokens: 890, status: 'success' },
|
||||
{ time: '14:28:12', client: 'client-3', provider: 'DeepSeek', model: 'deepseek-chat', tokens: 1560, status: 'error' },
|
||||
{ time: '14:25:33', client: 'client-1', provider: 'OpenAI', model: 'gpt-3.5-turbo', tokens: 540, status: 'success' },
|
||||
{ time: '14:22:18', client: 'client-4', provider: 'Grok', model: 'grok-beta', tokens: 720, status: 'success' },
|
||||
{ time: '14:20:05', client: 'client-2', provider: 'Gemini', model: 'gemini-pro-vision', tokens: 1120, status: 'success' },
|
||||
{ time: '14:18:47', client: 'client-5', provider: 'OpenAI', model: 'gpt-4', tokens: 980, status: 'warning' },
|
||||
{ time: '14:15:22', client: 'client-3', provider: 'DeepSeek', model: 'deepseek-coder', tokens: 1340, status: 'success' },
|
||||
{ time: '14:12:10', client: 'client-1', provider: 'OpenAI', model: 'gpt-3.5-turbo', tokens: 610, status: 'success' },
|
||||
{ time: '14:10:05', client: 'client-6', provider: 'Gemini', model: 'gemini-pro', tokens: 830, status: 'success' }
|
||||
];
|
||||
|
||||
this.renderRecentRequests(requests);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading recent requests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderRecentRequests(requests) {
|
||||
const tableBody = document.querySelector('#recent-requests tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = requests.map(request => {
|
||||
const statusClass = request.status === 'success' ? 'success' :
|
||||
request.status === 'error' ? 'danger' : 'warning';
|
||||
const statusIcon = request.status === 'success' ? 'check-circle' :
|
||||
request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${request.time}</td>
|
||||
<td>${request.client}</td>
|
||||
<td>${request.provider}</td>
|
||||
<td>${request.model}</td>
|
||||
<td>${request.tokens.toLocaleString()}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${request.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Period buttons for requests chart
|
||||
const periodButtons = document.querySelectorAll('.chart-control-btn[data-period]');
|
||||
periodButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Update active state
|
||||
periodButtons.forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
// Update chart based on period
|
||||
this.updateRequestsChart(button.dataset.period);
|
||||
});
|
||||
});
|
||||
|
||||
// Refresh button for recent requests
|
||||
const refreshBtn = document.querySelector('#recent-requests .card-action-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
this.loadRecentRequests();
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Recent requests refreshed', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupWebSocketSubscriptions() {
|
||||
if (!window.wsManager) return;
|
||||
|
||||
// Subscribe to request updates
|
||||
window.wsManager.subscribe('requests', (request) => {
|
||||
this.handleNewRequest(request);
|
||||
});
|
||||
|
||||
// Subscribe to metric updates
|
||||
window.wsManager.subscribe('metrics', (metric) => {
|
||||
this.handleNewMetric(metric);
|
||||
});
|
||||
}
|
||||
|
||||
handleNewRequest(request) {
|
||||
// Update total requests counter
|
||||
if (this.stats) {
|
||||
this.stats.totalRequests++;
|
||||
this.stats.todayRequests++;
|
||||
|
||||
// Update tokens if available
|
||||
if (request.tokens) {
|
||||
this.stats.totalTokens += request.tokens;
|
||||
}
|
||||
|
||||
// Re-render stats
|
||||
this.renderStats();
|
||||
}
|
||||
|
||||
// Add to recent requests table
|
||||
this.addToRecentRequests(request);
|
||||
}
|
||||
|
||||
addToRecentRequests(request) {
|
||||
const tableBody = document.querySelector('#recent-requests tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
const time = new Date(request.timestamp || Date.now()).toLocaleTimeString();
|
||||
const statusClass = request.status === 'success' ? 'success' :
|
||||
request.status === 'error' ? 'danger' : 'warning';
|
||||
const statusIcon = request.status === 'success' ? 'check-circle' :
|
||||
request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${time}</td>
|
||||
<td>${request.client_id || 'Unknown'}</td>
|
||||
<td>${request.provider || 'Unknown'}</td>
|
||||
<td>${request.model || 'Unknown'}</td>
|
||||
<td>${request.tokens || 0}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${request.status || 'unknown'}
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// Add to top of table
|
||||
tableBody.insertBefore(row, tableBody.firstChild);
|
||||
|
||||
// Limit to 50 rows
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
if (rows.length > 50) {
|
||||
tableBody.removeChild(rows[rows.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewMetric(metric) {
|
||||
// Update charts with new metric data
|
||||
if (metric.type === 'requests' && this.charts.requests) {
|
||||
this.updateRequestsChartData(metric);
|
||||
}
|
||||
|
||||
// Update system health if needed
|
||||
if (metric.type === 'system_health') {
|
||||
this.updateSystemHealth(metric);
|
||||
}
|
||||
}
|
||||
|
||||
updateRequestsChart(period) {
|
||||
// In a real app, this would fetch new data based on period
|
||||
// For now, just update with demo data
|
||||
let hours = 24;
|
||||
if (period === '7d') hours = 24 * 7;
|
||||
if (period === '30d') hours = 24 * 30;
|
||||
|
||||
const data = window.chartManager.generateDemoTimeSeries(hours, 1);
|
||||
data.datasets[0].label = 'Requests';
|
||||
data.datasets[0].fill = true;
|
||||
|
||||
window.chartManager.updateChartData('requests-chart', data);
|
||||
}
|
||||
|
||||
updateRequestsChartData(metric) {
|
||||
// Add new data point to the chart
|
||||
if (this.charts.requests && metric.value !== undefined) {
|
||||
window.chartManager.addDataPoint('requests-chart', metric.value);
|
||||
}
|
||||
}
|
||||
|
||||
updateSystemHealth(metric) {
|
||||
// Update system health indicators
|
||||
const container = document.getElementById('system-health');
|
||||
if (!container || !metric.data) return;
|
||||
|
||||
// This would update specific health indicators based on metric data
|
||||
// Implementation depends on metric structure
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const container = document.getElementById('overview-stats');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="error-message" style="grid-column: 1 / -1;">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadStats();
|
||||
this.loadRecentRequests();
|
||||
|
||||
// Refresh charts
|
||||
if (this.charts.requests) {
|
||||
this.charts.requests.update();
|
||||
}
|
||||
if (this.charts.providers) {
|
||||
this.charts.providers.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize overview page when needed
|
||||
window.initOverview = async () => {
|
||||
window.overviewPage = new OverviewPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = OverviewPage;
|
||||
}
|
||||
650
static/js/pages/providers.js
Normal file
650
static/js/pages/providers.js
Normal file
@@ -0,0 +1,650 @@
|
||||
// Providers Page Module
|
||||
|
||||
class ProvidersPage {
|
||||
constructor() {
|
||||
this.providers = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadProviderStats();
|
||||
await this.loadProvidersList();
|
||||
await this.loadModelsList();
|
||||
await this.loadConnectionTests();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadProviderStats() {
|
||||
const container = document.getElementById('provider-stats');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">4</div>
|
||||
<div class="stat-label">Total Providers</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
3 active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-plug"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">3</div>
|
||||
<div class="stat-label">Connected</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
All systems operational
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Issues</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
DeepSeek: 85% health
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Offline</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-redo"></i>
|
||||
Grok: Connection failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async loadProvidersList() {
|
||||
const container = document.getElementById('providers-list');
|
||||
if (!container) return;
|
||||
|
||||
this.providers = [
|
||||
{ name: 'OpenAI', enabled: true, status: 'online', apiKey: 'sk-*****123', models: ['gpt-4', 'gpt-3.5-turbo'], lastUsed: '2024-01-15 14:32:15' },
|
||||
{ name: 'Gemini', enabled: true, status: 'online', apiKey: 'AIza*****456', models: ['gemini-pro', 'gemini-pro-vision'], lastUsed: '2024-01-15 14:30:45' },
|
||||
{ name: 'DeepSeek', enabled: true, status: 'warning', apiKey: 'sk-*****789', models: ['deepseek-chat', 'deepseek-coder'], lastUsed: '2024-01-15 14:28:12' },
|
||||
{ name: 'Grok', enabled: false, status: 'offline', apiKey: 'gk-*****012', models: ['grok-beta'], lastUsed: '2024-01-12 10:15:22' }
|
||||
];
|
||||
|
||||
container.innerHTML = this.providers.map(provider => {
|
||||
const statusClass = provider.status === 'online' ? 'success' :
|
||||
provider.status === 'warning' ? 'warning' : 'danger';
|
||||
const statusIcon = provider.status === 'online' ? 'check-circle' :
|
||||
provider.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
|
||||
|
||||
return `
|
||||
<div class="provider-card">
|
||||
<div class="provider-header">
|
||||
<div class="provider-info">
|
||||
<h4 class="provider-name">${provider.name}</h4>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${provider.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" ${provider.enabled ? 'checked' : ''} data-provider="${provider.name}">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<button class="btn-action" title="Configure" data-action="configure" data-provider="${provider.name}">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<button class="btn-action" title="Test Connection" data-action="test" data-provider="${provider.name}">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">API Key:</span>
|
||||
<code class="detail-value">${provider.apiKey}</code>
|
||||
<button class="btn-copy" data-text="${provider.apiKey}" title="Copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Models:</span>
|
||||
<span class="detail-value">${provider.models.join(', ')}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Last Used:</span>
|
||||
<span class="detail-value">${provider.lastUsed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for provider cards
|
||||
this.addProviderStyles();
|
||||
}
|
||||
|
||||
addProviderStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.provider-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--text-light);
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.provider-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadModelsList() {
|
||||
const container = document.getElementById('models-list');
|
||||
if (!container) return;
|
||||
|
||||
const models = [
|
||||
{ provider: 'OpenAI', name: 'gpt-4', enabled: true, context: 8192, maxTokens: 4096 },
|
||||
{ provider: 'OpenAI', name: 'gpt-3.5-turbo', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'Gemini', name: 'gemini-pro', enabled: true, context: 32768, maxTokens: 8192 },
|
||||
{ provider: 'Gemini', name: 'gemini-pro-vision', enabled: true, context: 32768, maxTokens: 4096 },
|
||||
{ provider: 'DeepSeek', name: 'deepseek-chat', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'DeepSeek', name: 'deepseek-coder', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'Grok', name: 'grok-beta', enabled: false, context: 8192, maxTokens: 2048 }
|
||||
];
|
||||
|
||||
container.innerHTML = models.map(model => `
|
||||
<div class="model-item">
|
||||
<div class="model-header">
|
||||
<span class="model-name">${model.name}</span>
|
||||
<span class="model-provider">${model.provider}</span>
|
||||
</div>
|
||||
<div class="model-details">
|
||||
<span class="model-detail">
|
||||
<i class="fas fa-microchip"></i>
|
||||
Context: ${model.context.toLocaleString()} tokens
|
||||
</span>
|
||||
<span class="model-detail">
|
||||
<i class="fas fa-ruler"></i>
|
||||
Max: ${model.maxTokens.toLocaleString()} tokens
|
||||
</span>
|
||||
<span class="model-status ${model.enabled ? 'enabled' : 'disabled'}">
|
||||
<i class="fas fa-${model.enabled ? 'check' : 'times'}"></i>
|
||||
${model.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for model items
|
||||
this.addModelStyles();
|
||||
}
|
||||
|
||||
addModelStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.model-item {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.model-provider {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--bg-primary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.model-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.model-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.model-detail i {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.model-status {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.model-status.enabled {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.model-status.disabled {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: var(--danger);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadConnectionTests() {
|
||||
const container = document.getElementById('connection-tests');
|
||||
if (!container) return;
|
||||
|
||||
const tests = [
|
||||
{ provider: 'OpenAI', status: 'success', latency: 245, timestamp: '2024-01-15 14:35:00' },
|
||||
{ provider: 'Gemini', status: 'success', latency: 189, timestamp: '2024-01-15 14:34:30' },
|
||||
{ provider: 'DeepSeek', status: 'warning', latency: 520, timestamp: '2024-01-15 14:34:00' },
|
||||
{ provider: 'Grok', status: 'error', latency: null, timestamp: '2024-01-15 14:33:30' }
|
||||
];
|
||||
|
||||
container.innerHTML = tests.map(test => {
|
||||
const statusClass = test.status === 'success' ? 'success' :
|
||||
test.status === 'warning' ? 'warning' : 'danger';
|
||||
const statusIcon = test.status === 'success' ? 'check-circle' :
|
||||
test.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
|
||||
|
||||
return `
|
||||
<div class="test-result">
|
||||
<div class="test-provider">${test.provider}</div>
|
||||
<div class="test-status">
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${test.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="test-latency">${test.latency ? `${test.latency}ms` : 'N/A'}</div>
|
||||
<div class="test-time">${test.timestamp}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for test results
|
||||
this.addTestStyles();
|
||||
}
|
||||
|
||||
addTestStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.test-result {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 2fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.test-result:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.test-provider {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.test-latency {
|
||||
color: var(--text-secondary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.test-time {
|
||||
color: var(--text-light);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Test all providers button
|
||||
const testAllBtn = document.getElementById('test-all-providers');
|
||||
if (testAllBtn) {
|
||||
testAllBtn.addEventListener('click', () => {
|
||||
this.testAllProviders();
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle switches
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.matches('.toggle-switch input')) {
|
||||
const provider = e.target.dataset.provider;
|
||||
const enabled = e.target.checked;
|
||||
this.toggleProvider(provider, enabled);
|
||||
}
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-action')) {
|
||||
const button = e.target.closest('.btn-action');
|
||||
const action = button.dataset.action;
|
||||
const provider = button.dataset.provider;
|
||||
|
||||
switch (action) {
|
||||
case 'configure':
|
||||
this.configureProvider(provider);
|
||||
break;
|
||||
case 'test':
|
||||
this.testProvider(provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy buttons
|
||||
if (e.target.closest('.btn-copy')) {
|
||||
const button = e.target.closest('.btn-copy');
|
||||
const text = button.dataset.text;
|
||||
this.copyToClipboard(text);
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Copied to clipboard', 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleProvider(providerName, enabled) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// In a real app, this would update the provider via API
|
||||
provider.enabled = enabled;
|
||||
provider.status = enabled ? 'online' : 'offline';
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(
|
||||
`${providerName} ${enabled ? 'enabled' : 'disabled'}`,
|
||||
enabled ? 'success' : 'warning'
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh providers list
|
||||
this.loadProvidersList();
|
||||
}
|
||||
|
||||
configureProvider(providerName) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// Show configuration modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Configure ${providerName}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="configure-provider-form">
|
||||
<div class="form-control">
|
||||
<label for="api-key">API Key</label>
|
||||
<input type="password" id="api-key" value="${provider.apiKey}" placeholder="Enter API key" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="base-url">Base URL (Optional)</label>
|
||||
<input type="text" id="base-url" placeholder="https://api.openai.com/v1">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="timeout">Timeout (seconds)</label>
|
||||
<input type="number" id="timeout" value="30" min="1" max="300">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="retry-count">Retry Count</label>
|
||||
<input type="number" id="retry-count" value="3" min="0" max="10">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary save-config">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const saveBtn = modal.querySelector('.save-config');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// In a real app, this would save provider configuration
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`${providerName} configuration saved`, 'success');
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testProvider(providerName) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// Show testing in progress
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Testing ${providerName} connection...`, 'info');
|
||||
}
|
||||
|
||||
// Simulate API test
|
||||
setTimeout(() => {
|
||||
// In a real app, this would test the provider connection via API
|
||||
const success = Math.random() > 0.3; // 70% success rate for demo
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(
|
||||
`${providerName} connection ${success ? 'successful' : 'failed'}`,
|
||||
success ? 'success' : 'error'
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh connection tests
|
||||
this.loadConnectionTests();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
testAllProviders() {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Testing all providers...', 'info');
|
||||
}
|
||||
|
||||
// Test each provider sequentially
|
||||
this.providers.forEach((provider, index) => {
|
||||
setTimeout(() => {
|
||||
this.testProvider(provider.name);
|
||||
}, index * 2000); // Stagger tests
|
||||
});
|
||||
}
|
||||
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadProviderStats();
|
||||
this.loadProvidersList();
|
||||
this.loadModelsList();
|
||||
this.loadConnectionTests();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize providers page when needed
|
||||
window.initProviders = async () => {
|
||||
window.providersPage = new ProvidersPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ProvidersPage;
|
||||
}
|
||||
318
static/js/pages/settings.js
Normal file
318
static/js/pages/settings.js
Normal file
@@ -0,0 +1,318 @@
|
||||
// Settings Page Module
|
||||
|
||||
class SettingsPage {
|
||||
constructor() {
|
||||
this.settings = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load settings
|
||||
await this.loadSettings();
|
||||
await this.loadSystemInfo();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/settings
|
||||
this.settings = {
|
||||
serverPort: 8080,
|
||||
logLevel: 'info',
|
||||
dbPath: './data/llm-proxy.db',
|
||||
backupInterval: 24,
|
||||
sessionTimeout: 30,
|
||||
enableRateLimiting: true,
|
||||
enableCostTracking: true,
|
||||
enableMetrics: true,
|
||||
enableWebSocket: true
|
||||
};
|
||||
|
||||
this.renderSettingsForm();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderSettingsForm() {
|
||||
const form = document.getElementById('settings-form');
|
||||
if (!form) return;
|
||||
|
||||
// Server port
|
||||
const portInput = document.getElementById('server-port');
|
||||
if (portInput) portInput.value = this.settings.serverPort;
|
||||
|
||||
// Log level
|
||||
const logLevelSelect = document.getElementById('log-level');
|
||||
if (logLevelSelect) logLevelSelect.value = this.settings.logLevel;
|
||||
|
||||
// Database path
|
||||
const dbPathInput = document.getElementById('db-path');
|
||||
if (dbPathInput) dbPathInput.value = this.settings.dbPath;
|
||||
|
||||
// Backup interval
|
||||
const backupInput = document.getElementById('backup-interval');
|
||||
if (backupInput) backupInput.value = this.settings.backupInterval;
|
||||
|
||||
// Session timeout
|
||||
const sessionInput = document.getElementById('session-timeout');
|
||||
if (sessionInput) sessionInput.value = this.settings.sessionTimeout;
|
||||
}
|
||||
|
||||
async loadSystemInfo() {
|
||||
const container = document.getElementById('system-info');
|
||||
if (!container) return;
|
||||
|
||||
// In a real app, this would fetch system information
|
||||
const systemInfo = {
|
||||
version: '1.0.0',
|
||||
uptime: '5 days, 3 hours',
|
||||
platform: 'Linux x86_64',
|
||||
node: 'v18.17.0',
|
||||
memory: '2.4 GB / 8.0 GB',
|
||||
disk: '45 GB / 256 GB',
|
||||
lastBackup: '2024-01-15 02:00:00',
|
||||
lastRestart: '2024-01-10 14:30:00'
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Version:</span>
|
||||
<span class="info-value">${systemInfo.version}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Uptime:</span>
|
||||
<span class="info-value">${systemInfo.uptime}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Platform:</span>
|
||||
<span class="info-value">${systemInfo.platform}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Node.js:</span>
|
||||
<span class="info-value">${systemInfo.node}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Memory:</span>
|
||||
<span class="info-value">${systemInfo.memory}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Disk:</span>
|
||||
<span class="info-value">${systemInfo.disk}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Backup:</span>
|
||||
<span class="info-value">${systemInfo.lastBackup}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Restart:</span>
|
||||
<span class="info-value">${systemInfo.lastRestart}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add CSS for info grid
|
||||
this.addInfoStyles();
|
||||
}
|
||||
|
||||
addInfoStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Settings form
|
||||
const form = document.getElementById('settings-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Reset settings button
|
||||
const resetBtn = document.getElementById('reset-settings');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.resetSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Database management buttons
|
||||
const backupBtn = document.getElementById('backup-db');
|
||||
if (backupBtn) {
|
||||
backupBtn.addEventListener('click', () => {
|
||||
this.backupDatabase();
|
||||
});
|
||||
}
|
||||
|
||||
const optimizeBtn = document.getElementById('optimize-db');
|
||||
if (optimizeBtn) {
|
||||
optimizeBtn.addEventListener('click', () => {
|
||||
this.optimizeDatabase();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
// Collect form values
|
||||
const settings = {
|
||||
serverPort: parseInt(document.getElementById('server-port').value) || 8080,
|
||||
logLevel: document.getElementById('log-level').value,
|
||||
dbPath: document.getElementById('db-path').value,
|
||||
backupInterval: parseInt(document.getElementById('backup-interval').value) || 24,
|
||||
sessionTimeout: parseInt(document.getElementById('session-timeout').value) || 30,
|
||||
dashboardPassword: document.getElementById('dashboard-password').value
|
||||
};
|
||||
|
||||
// Validate settings
|
||||
if (settings.serverPort < 1024 || settings.serverPort > 65535) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Server port must be between 1024 and 65535', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.backupInterval < 1 || settings.backupInterval > 168) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Backup interval must be between 1 and 168 hours', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.sessionTimeout < 5 || settings.sessionTimeout > 1440) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Session timeout must be between 5 and 1440 minutes', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real app, this would save settings via API
|
||||
this.settings = { ...this.settings, ...settings };
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Settings saved successfully', 'success');
|
||||
}
|
||||
|
||||
// Clear password field
|
||||
document.getElementById('dashboard-password').value = '';
|
||||
}
|
||||
|
||||
resetSettings() {
|
||||
if (confirm('Are you sure you want to reset all settings to default values?')) {
|
||||
// Reset to defaults
|
||||
this.settings = {
|
||||
serverPort: 8080,
|
||||
logLevel: 'info',
|
||||
dbPath: './data/llm-proxy.db',
|
||||
backupInterval: 24,
|
||||
sessionTimeout: 30,
|
||||
enableRateLimiting: true,
|
||||
enableCostTracking: true,
|
||||
enableMetrics: true,
|
||||
enableWebSocket: true
|
||||
};
|
||||
|
||||
this.renderSettingsForm();
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Settings reset to defaults', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backupDatabase() {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Starting database backup...', 'info');
|
||||
}
|
||||
|
||||
// Simulate backup process
|
||||
setTimeout(() => {
|
||||
// In a real app, this would trigger a database backup via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Database backup completed successfully', 'success');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
optimizeDatabase() {
|
||||
if (confirm('Optimize database? This may improve performance but could take a few moments.')) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Optimizing database...', 'info');
|
||||
}
|
||||
|
||||
// Simulate optimization process
|
||||
setTimeout(() => {
|
||||
// In a real app, this would optimize the database via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Database optimization completed', 'success');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadSettings();
|
||||
this.loadSystemInfo();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize settings page when needed
|
||||
window.initSettings = async () => {
|
||||
window.settingsPage = new SettingsPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SettingsPage;
|
||||
}
|
||||
510
static/js/websocket.js
Normal file
510
static/js/websocket.js
Normal file
@@ -0,0 +1,510 @@
|
||||
// WebSocket Manager for Real-time Updates
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 1000;
|
||||
this.isConnected = false;
|
||||
this.subscribers = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.connect();
|
||||
this.setupStatusIndicator();
|
||||
this.setupAutoReconnect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
try {
|
||||
// Determine WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
const wsUrl = `${protocol}//${host}/ws`;
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => this.onOpen();
|
||||
this.ws.onclose = () => this.onClose();
|
||||
this.ws.onerror = (error) => this.onError(error);
|
||||
this.ws.onmessage = (event) => this.onMessage(event);
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
console.log('WebSocket connected');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.updateStatus('connected');
|
||||
|
||||
// Notify subscribers
|
||||
this.notify('connection', { status: 'connected' });
|
||||
|
||||
// Send authentication if needed
|
||||
if (window.authManager && window.authManager.token) {
|
||||
this.send({
|
||||
type: 'auth',
|
||||
token: window.authManager.token
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to default channels
|
||||
this.send({
|
||||
type: 'subscribe',
|
||||
channels: ['requests', 'metrics', 'logs']
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
console.log('WebSocket disconnected');
|
||||
this.isConnected = false;
|
||||
this.updateStatus('disconnected');
|
||||
|
||||
// Notify subscribers
|
||||
this.notify('connection', { status: 'disconnected' });
|
||||
|
||||
// Schedule reconnection
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
this.updateStatus('error');
|
||||
}
|
||||
|
||||
onMessage(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
const { type, channel, payload } = data;
|
||||
|
||||
// Notify channel subscribers
|
||||
if (channel && this.subscribers.has(channel)) {
|
||||
this.subscribers.get(channel).forEach(callback => {
|
||||
try {
|
||||
callback(payload);
|
||||
} catch (error) {
|
||||
console.error('Error in WebSocket callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle specific message types
|
||||
switch (type) {
|
||||
case 'request':
|
||||
this.handleRequest(payload);
|
||||
break;
|
||||
case 'metric':
|
||||
this.handleMetric(payload);
|
||||
break;
|
||||
case 'log':
|
||||
this.handleLog(payload);
|
||||
break;
|
||||
case 'system':
|
||||
this.handleSystem(payload);
|
||||
break;
|
||||
case 'error':
|
||||
this.handleError(payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(request) {
|
||||
// Update request counters
|
||||
this.updateRequestCounters(request);
|
||||
|
||||
// Add to recent requests if on overview page
|
||||
if (window.dashboard && window.dashboard.currentPage === 'overview') {
|
||||
this.addRecentRequest(request);
|
||||
}
|
||||
|
||||
// Update monitoring stream if on monitoring page
|
||||
if (window.dashboard && window.dashboard.currentPage === 'monitoring') {
|
||||
this.addToMonitoringStream(request);
|
||||
}
|
||||
}
|
||||
|
||||
handleMetric(metric) {
|
||||
// Update charts with new metric data
|
||||
this.updateCharts(metric);
|
||||
|
||||
// Update system metrics display
|
||||
this.updateSystemMetrics(metric);
|
||||
}
|
||||
|
||||
handleLog(log) {
|
||||
// Add to logs table if on logs page
|
||||
if (window.dashboard && window.dashboard.currentPage === 'logs') {
|
||||
this.addLogEntry(log);
|
||||
}
|
||||
|
||||
// Add to monitoring logs if on monitoring page
|
||||
if (window.dashboard && window.dashboard.currentPage === 'monitoring') {
|
||||
this.addToLogStream(log);
|
||||
}
|
||||
}
|
||||
|
||||
handleSystem(system) {
|
||||
// Update system health indicators
|
||||
this.updateSystemHealth(system);
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
console.error('Server error:', error);
|
||||
|
||||
// Show error toast
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(error.message || 'Server error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(data));
|
||||
return true;
|
||||
} else {
|
||||
console.warn('WebSocket not connected, message not sent:', data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(channel, callback) {
|
||||
if (!this.subscribers.has(channel)) {
|
||||
this.subscribers.set(channel, new Set());
|
||||
}
|
||||
this.subscribers.get(channel).add(callback);
|
||||
|
||||
// Send subscription to server
|
||||
this.send({
|
||||
type: 'subscribe',
|
||||
channels: [channel]
|
||||
});
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => this.unsubscribe(channel, callback);
|
||||
}
|
||||
|
||||
unsubscribe(channel, callback) {
|
||||
if (this.subscribers.has(channel)) {
|
||||
this.subscribers.get(channel).delete(callback);
|
||||
|
||||
// If no more subscribers, unsubscribe from server
|
||||
if (this.subscribers.get(channel).size === 0) {
|
||||
this.send({
|
||||
type: 'unsubscribe',
|
||||
channels: [channel]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notify(channel, data) {
|
||||
if (this.subscribers.has(channel)) {
|
||||
this.subscribers.get(channel).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error('Error in notification callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scheduleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.warn('Max reconnection attempts reached');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
||||
|
||||
console.log(`Scheduling reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) {
|
||||
this.connect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
setupAutoReconnect() {
|
||||
// Reconnect when browser comes online
|
||||
window.addEventListener('online', () => {
|
||||
if (!this.isConnected) {
|
||||
console.log('Browser online, attempting to reconnect...');
|
||||
this.connect();
|
||||
}
|
||||
});
|
||||
|
||||
// Keepalive ping
|
||||
setInterval(() => {
|
||||
if (this.isConnected) {
|
||||
this.send({ type: 'ping' });
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
setupStatusIndicator() {
|
||||
// Status indicator is already in the HTML
|
||||
// This function just ensures it's properly styled
|
||||
}
|
||||
|
||||
updateStatus(status) {
|
||||
const statusElement = document.getElementById('ws-status');
|
||||
if (!statusElement) return;
|
||||
|
||||
const dot = statusElement.querySelector('.ws-dot');
|
||||
const text = statusElement.querySelector('.ws-text');
|
||||
|
||||
if (!dot || !text) return;
|
||||
|
||||
// Remove all status classes
|
||||
dot.classList.remove('connected', 'disconnected');
|
||||
statusElement.classList.remove('connected', 'disconnected');
|
||||
|
||||
// Add new status class
|
||||
dot.classList.add(status);
|
||||
statusElement.classList.add(status);
|
||||
|
||||
// Update text
|
||||
const statusText = {
|
||||
'connected': 'Connected',
|
||||
'disconnected': 'Disconnected',
|
||||
'connecting': 'Connecting...',
|
||||
'error': 'Connection Error'
|
||||
};
|
||||
|
||||
text.textContent = statusText[status] || status;
|
||||
}
|
||||
|
||||
// Helper methods for updating UI
|
||||
updateRequestCounters(request) {
|
||||
// Update request counters in overview stats
|
||||
const requestCountElement = document.querySelector('[data-stat="total-requests"]');
|
||||
if (requestCountElement) {
|
||||
const currentCount = parseInt(requestCountElement.textContent) || 0;
|
||||
requestCountElement.textContent = currentCount + 1;
|
||||
}
|
||||
|
||||
// Update token counters
|
||||
const tokenCountElement = document.querySelector('[data-stat="total-tokens"]');
|
||||
if (tokenCountElement && request.tokens) {
|
||||
const currentTokens = parseInt(tokenCountElement.textContent) || 0;
|
||||
tokenCountElement.textContent = currentTokens + request.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
addRecentRequest(request) {
|
||||
const tableBody = document.querySelector('#recent-requests tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Format time
|
||||
const time = new Date(request.timestamp || Date.now()).toLocaleTimeString();
|
||||
|
||||
// Format status badge
|
||||
const statusClass = request.status === 'success' ? 'success' :
|
||||
request.status === 'error' ? 'danger' : 'warning';
|
||||
const statusIcon = request.status === 'success' ? 'check-circle' :
|
||||
request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${time}</td>
|
||||
<td>${request.client_id || 'Unknown'}</td>
|
||||
<td>${request.provider || 'Unknown'}</td>
|
||||
<td>${request.model || 'Unknown'}</td>
|
||||
<td>${request.tokens || 0}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${request.status || 'unknown'}
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// Add to top of table
|
||||
tableBody.insertBefore(row, tableBody.firstChild);
|
||||
|
||||
// Limit to 50 rows
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
if (rows.length > 50) {
|
||||
tableBody.removeChild(rows[rows.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
addToMonitoringStream(request) {
|
||||
const streamElement = document.getElementById('request-stream');
|
||||
if (!streamElement) return;
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'stream-entry';
|
||||
|
||||
// Format time
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// Determine icon based on status
|
||||
let icon = 'question-circle';
|
||||
let color = 'var(--text-secondary)';
|
||||
|
||||
if (request.status === 'success') {
|
||||
icon = 'check-circle';
|
||||
color = 'var(--success)';
|
||||
} else if (request.status === 'error') {
|
||||
icon = 'exclamation-circle';
|
||||
color = 'var(--danger)';
|
||||
}
|
||||
|
||||
entry.innerHTML = `
|
||||
<div class="stream-entry-time">${time}</div>
|
||||
<div class="stream-entry-icon" style="color: ${color}">
|
||||
<i class="fas fa-${icon}"></i>
|
||||
</div>
|
||||
<div class="stream-entry-content">
|
||||
<strong>${request.client_id || 'Unknown'}</strong> →
|
||||
${request.provider || 'Unknown'} (${request.model || 'Unknown'})
|
||||
<div class="stream-entry-details">
|
||||
${request.tokens || 0} tokens • ${request.duration || 0}ms
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to top of stream
|
||||
streamElement.insertBefore(entry, streamElement.firstChild);
|
||||
|
||||
// Limit to 20 entries
|
||||
const entries = streamElement.querySelectorAll('.stream-entry');
|
||||
if (entries.length > 20) {
|
||||
streamElement.removeChild(entries[entries.length - 1]);
|
||||
}
|
||||
|
||||
// Add highlight animation
|
||||
entry.classList.add('highlight');
|
||||
setTimeout(() => entry.classList.remove('highlight'), 1000);
|
||||
}
|
||||
|
||||
updateCharts(metric) {
|
||||
// This would update Chart.js charts with new data
|
||||
// Implementation depends on specific chart setup
|
||||
}
|
||||
|
||||
updateSystemMetrics(metric) {
|
||||
const metricsElement = document.getElementById('system-metrics');
|
||||
if (!metricsElement) return;
|
||||
|
||||
// Update specific metric displays
|
||||
// This is a simplified example
|
||||
}
|
||||
|
||||
addLogEntry(log) {
|
||||
const tableBody = document.querySelector('#logs-table tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Format time
|
||||
const time = new Date(log.timestamp || Date.now()).toLocaleString();
|
||||
|
||||
// Determine log level class
|
||||
const levelClass = log.level || 'info';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${time}</td>
|
||||
<td>
|
||||
<span class="status-badge ${levelClass}">
|
||||
${levelClass.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td>${log.source || 'Unknown'}</td>
|
||||
<td>${log.message || ''}</td>
|
||||
`;
|
||||
|
||||
// Add to top of table
|
||||
tableBody.insertBefore(row, tableBody.firstChild);
|
||||
|
||||
// Limit to 100 rows
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
if (rows.length > 100) {
|
||||
tableBody.removeChild(rows[rows.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
addToLogStream(log) {
|
||||
const logStreamElement = document.getElementById('system-logs');
|
||||
if (!logStreamElement) return;
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry log-${log.level || 'info'}`;
|
||||
|
||||
// Format time
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// Determine icon based on level
|
||||
let icon = 'info-circle';
|
||||
if (log.level === 'error') icon = 'exclamation-circle';
|
||||
if (log.level === 'warn') icon = 'exclamation-triangle';
|
||||
if (log.level === 'debug') icon = 'bug';
|
||||
|
||||
entry.innerHTML = `
|
||||
<div class="log-time">${time}</div>
|
||||
<div class="log-level">
|
||||
<i class="fas fa-${icon}"></i>
|
||||
</div>
|
||||
<div class="log-message">${log.message || ''}</div>
|
||||
`;
|
||||
|
||||
// Add to top of stream
|
||||
logStreamElement.insertBefore(entry, logStreamElement.firstChild);
|
||||
|
||||
// Limit to 50 entries
|
||||
const entries = logStreamElement.querySelectorAll('.log-entry');
|
||||
if (entries.length > 50) {
|
||||
logStreamElement.removeChild(entries[entries.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
updateSystemHealth(system) {
|
||||
const healthElement = document.getElementById('system-health');
|
||||
if (!healthElement) return;
|
||||
|
||||
// Update system health indicators
|
||||
// This is a simplified example
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.updateStatus('disconnected');
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.disconnect();
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize WebSocket manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.wsManager = new WebSocketManager();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = WebSocketManager;
|
||||
}
|
||||
Reference in New Issue
Block a user