Init repo

This commit is contained in:
2026-02-26 11:51:36 -05:00
commit 5400d82acd
50 changed files with 17748 additions and 0 deletions

View 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
View 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
View 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
View 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;
}

View 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
View 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;
}

View 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
View 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;
}