amauricunha's picture
Upload 2 files
6f713c1 verified
Raw
History Blame Contribute Delete
49 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>English Helper - Admin Panel</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.admin-card {
@apply bg-white rounded-lg shadow-md p-6 border border-gray-200;
}
.stat-card {
@apply bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg p-4 shadow-md;
}
.danger-zone {
@apply bg-red-50 border border-red-200 rounded-lg p-4;
}
.success-zone {
@apply bg-green-50 border border-green-200 rounded-lg p-4;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.table-container {
max-height: 600px;
overflow-y: auto;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Admin Login Modal -->
<div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
<h2 class="text-2xl font-bold mb-6 text-center">🔐 Admin Login</h2>
<form id="adminLoginForm">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">Username</label>
<input type="text" id="adminUsername" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500" required>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">Password</label>
<input type="password" id="adminPassword" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500" required>
</div>
<button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition duration-200">
🚀 Login
</button>
</form>
<div id="loginError" class="mt-4 text-red-600 text-sm hidden"></div>
</div>
</div>
<!-- Admin Interface -->
<div id="adminInterface" class="hidden">
<!-- Header -->
<header class="bg-white shadow-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
</div>
<div class="flex items-center space-x-4">
<span id="adminUserInfo" class="text-gray-600"></span>
<button onclick="adminLogout()" class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition duration-200">
🚪 Logout
</button>
</div>
</div>
</div>
</header>
<!-- Navigation Tabs -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
📊 Dashboard
</button>
<button onclick="showTab('users')" class="admin-tab" data-tab="users">
👥 Users
</button>
<button onclick="showTab('database')" class="admin-tab" data-tab="database">
🗄️ Database
</button>
<button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
🎯 Token Usage
</button>
<button onclick="showTab('system')" class="admin-tab" data-tab="system">
🖥️ System Health
</button>
</nav>
</div>
</div>
<!-- Tab Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Dashboard Tab -->
<div id="dashboardTab" class="tab-content">
<h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
<!-- System Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
<p id="totalUsers" class="text-3xl font-bold">-</p>
<p class="text-sm opacity-80">
<span id="newUsersWeek">-</span> this week
</p>
</div>
<div class="stat-card">
<h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
<p id="totalSessions" class="text-3xl font-bold">-</p>
<p class="text-sm opacity-80">All time</p>
</div>
<div class="stat-card">
<h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
<p id="totalTokens" class="text-3xl font-bold">-</p>
<p class="text-sm opacity-80">
$<span id="estimatedCost">-</span> estimated cost
</p>
</div>
<div class="stat-card">
<h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
<p id="totalContent" class="text-3xl font-bold">-</p>
<p class="text-sm opacity-80">Articles & flashcards</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
<canvas id="userGrowthChart" width="400" height="200"></canvas>
</div>
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
<canvas id="tokenCostChart" width="400" height="200"></canvas>
</div>
</div>
<!-- Recent Activity -->
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
<div id="recentActivity" class="space-y-2">
<div class="text-center text-gray-500 py-4">Loading recent activity...</div>
</div>
</div>
</div>
<!-- Users Tab -->
<div id="usersTab" class="tab-content hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">👥 User Management</h2>
<div class="flex items-center space-x-4">
<input type="text" id="userSearch" placeholder="Search users..." class="px-4 py-2 border rounded-lg">
<button onclick="refreshUsers()" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
🔄 Refresh
</button>
<button onclick="exportUsers()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
📥 Export CSV
</button>
</div>
</div>
<!-- Users Table -->
<div class="admin-card">
<div class="table-container">
<table class="w-full table-auto">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Activity</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr>
<td colspan="5" class="text-center py-8">
<div class="spinner"></div> Loading users...
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div id="usersPagination" class="flex justify-between items-center mt-4 pt-4 border-t">
<div id="usersInfo" class="text-sm text-gray-600"></div>
<div class="flex space-x-2">
<button onclick="previousPage()" id="prevPageBtn" class="px-3 py-1 bg-gray-300 rounded hover:bg-gray-400 disabled:opacity-50" disabled>Previous</button>
<span id="pageInfo" class="px-3 py-1">-</span>
<button onclick="nextPage()" id="nextPageBtn" class="px-3 py-1 bg-gray-300 rounded hover:bg-gray-400 disabled:opacity-50" disabled>Next</button>
</div>
</div>
</div>
</div>
<!-- Database Tab -->
<div id="databaseTab" class="tab-content hidden">
<h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
<div id="databaseSchema" class="space-y-4">
<div class="text-center text-gray-500 py-4">Loading schema...</div>
</div>
</div>
</div>
<!-- Token Usage Tab -->
<div id="tokensTab" class="tab-content hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
<button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
📥 Export Token Data
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
<canvas id="providerUsageChart" width="400" height="300"></canvas>
</div>
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
<canvas id="costBreakdownChart" width="400" height="300"></canvas>
</div>
</div>
</div>
<!-- System Health Tab -->
<div id="systemTab" class="tab-content hidden">
<h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
<!-- System Alerts -->
<div class="admin-card mb-6">
<h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
<div id="systemAlerts">
<div class="text-center text-gray-500 py-4">Loading alerts...</div>
</div>
</div>
<!-- System Metrics -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
<div class="admin-card">
<h4 class="font-semibold mb-3">💾 Memory Usage</h4>
<div id="memoryMetrics">
<div class="text-sm text-gray-500">Loading...</div>
</div>
</div>
<div class="admin-card">
<h4 class="font-semibold mb-3">💿 Disk Usage</h4>
<div id="diskMetrics">
<div class="text-sm text-gray-500">Loading...</div>
</div>
</div>
<div class="admin-card">
<h4 class="font-semibold mb-3">🗄️ Database</h4>
<div id="databaseMetrics">
<div class="text-sm text-gray-500">Loading...</div>
</div>
</div>
</div>
<!-- Error Logs -->
<div class="admin-card">
<h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
<div id="errorLogs">
<div class="text-center text-gray-500 py-4">Loading error logs...</div>
</div>
</div>
</div>
</div>
</div>
<!-- User Detail Modal -->
<div id="userDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-screen overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">👤 User Details</h3>
<button onclick="closeUserModal()" class="text-gray-500 hover:text-gray-700"></button>
</div>
<div id="userDetailContent">
<div class="text-center py-8">Loading user details...</div>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
<script>
let currentUser = null;
let currentPage = 1;
let usersData = null;
// Check admin authentication on load
document.addEventListener('DOMContentLoaded', () => {
checkAdminAuth();
});
async function checkAdminAuth() {
try {
const response = await fetch('/admin/check');
const data = await response.json();
if (data.authenticated) {
showAdminInterface(data.username);
} else {
showLoginModal();
}
} catch (error) {
console.error('Auth check error:', error);
showLoginModal();
}
}
function showLoginModal() {
document.getElementById('loginModal').classList.remove('hidden');
document.getElementById('adminInterface').classList.add('hidden');
}
function showAdminInterface(username) {
document.getElementById('loginModal').classList.add('hidden');
document.getElementById('adminInterface').classList.remove('hidden');
document.getElementById('adminUserInfo').textContent = `Welcome, ${username}`;
// Load dashboard data
loadDashboard();
}
// Admin login
document.getElementById('adminLoginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('adminUsername').value;
const password = document.getElementById('adminPassword').value;
const errorDiv = document.getElementById('loginError');
try {
const response = await fetch('/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
showAdminInterface(username);
errorDiv.classList.add('hidden');
} else {
errorDiv.textContent = data.error || 'Login failed';
errorDiv.classList.remove('hidden');
}
} catch (error) {
errorDiv.textContent = 'Network error. Please try again.';
errorDiv.classList.remove('hidden');
}
});
async function adminLogout() {
try {
await fetch('/admin/logout', { method: 'POST' });
showLoginModal();
showToast('Logged out successfully', 'success');
} catch (error) {
console.error('Logout error:', error);
}
}
// Tab management
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.add('hidden');
});
// Remove active class from all tab buttons
document.querySelectorAll('.admin-tab').forEach(btn => {
btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
btn.classList.add('border-transparent', 'text-gray-500');
});
// Show selected tab
document.getElementById(tabName + 'Tab').classList.remove('hidden');
// Add active class to selected tab button
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
activeBtn.classList.remove('border-transparent', 'text-gray-500');
// Load tab-specific data
if (tabName === 'dashboard') {
loadDashboard();
} else if (tabName === 'users') {
loadUsers();
} else if (tabName === 'database') {
loadDatabaseSchema();
} else if (tabName === 'tokens') {
loadTokenUsage();
} else if (tabName === 'system') {
loadSystemHealth();
}
}
async function loadDashboard() {
try {
const response = await fetch('/admin/dashboard');
const data = await response.json();
if (data.success) {
const stats = data.stats;
// Update stat cards
document.getElementById('totalUsers').textContent = stats.users?.total || 0;
document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
document.getElementById('totalContent').textContent =
(stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
// Update recent activity
updateRecentActivity(stats.recent_activity || []);
// Create charts
createCharts(stats);
}
} catch (error) {
console.error('Dashboard loading error:', error);
showToast('Failed to load dashboard', 'error');
}
}
function updateRecentActivity(activities) {
const container = document.getElementById('recentActivity');
if (activities.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
return;
}
container.innerHTML = activities.map(activity => `
<div class="flex items-center justify-between py-2 border-b border-gray-100">
<div>
<span class="font-medium">${activity.user}</span>
<span class="text-gray-600">performed ${activity.activity}</span>
</div>
<span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
</div>
`).join('');
}
async function loadUsers() {
try {
const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
const data = await response.json();
if (data.success) {
usersData = data.data;
updateUsersTable(usersData.users);
updateUsersPagination(usersData);
}
} catch (error) {
console.error('Users loading error:', error);
showToast('Failed to load users', 'error');
}
}
function updateUsersTable(users) {
const tbody = document.getElementById('usersTableBody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3">
<div>
<div class="font-medium">${user.email}</div>
<div class="text-sm text-gray-500">ID: ${user.id}</div>
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-600">
${formatDate(user.created_at)}
</td>
<td class="px-4 py-3">
<div class="text-sm">
<div>${user.session_count} sessions</div>
<div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
</div>
</td>
<td class="px-4 py-3">
<span class="px-2 py-1 text-xs rounded-full ${user.email_confirmed ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
</span>
</td>
<td class="px-4 py-3">
<div class="flex space-x-2">
<button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
<button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
</div>
</td>
</tr>
`).join('');
}
function updateUsersPagination(data) {
document.getElementById('usersInfo').textContent =
`Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
document.getElementById('prevPageBtn').disabled = data.page <= 1;
document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
}
async function viewUser(userId) {
try {
const response = await fetch(`/admin/users/${userId}`);
const data = await response.json();
if (data.success) {
showUserModal(data.user);
}
} catch (error) {
console.error('User details error:', error);
showToast('Failed to load user details', 'error');
}
}
function showUserModal(user) {
const modal = document.getElementById('userDetailModal');
const content = document.getElementById('userDetailContent');
content.innerHTML = `
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold mb-3">📋 Basic Information</h4>
<div class="space-y-2 text-sm">
<div><strong>Email:</strong> ${user.user.email}</div>
<div><strong>ID:</strong> ${user.user.id}</div>
<div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
<div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
<div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
</div>
</div>
<div>
<h4 class="font-semibold mb-3">⚙️ Settings</h4>
<div class="space-y-2 text-sm">
${Object.entries(user.settings).map(([key, value]) =>
`<div><strong>${key}:</strong> ${value}</div>`
).join('')}
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3">📊 Recent Activity</h4>
<div class="max-h-48 overflow-y-auto">
${user.recent_sessions.map(session => `
<div class="flex justify-between py-2 border-b text-sm">
<span>${session.activity}</span>
<span class="text-gray-500">${formatDate(session.timestamp)}</span>
</div>
`).join('')}
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3">🎯 Token Usage</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
${user.token_usage.map(usage => `
<div class="bg-gray-50 p-3 rounded">
<div class="font-medium">${usage.provider}</div>
<div class="text-sm text-gray-600">
Input: ${usage.input_tokens.toLocaleString()}<br>
Output: ${usage.output_tokens.toLocaleString()}<br>
Calls: ${usage.calls}
</div>
</div>
`).join('')}
</div>
</div>
`;
modal.classList.remove('hidden');
}
function closeUserModal() {
document.getElementById('userDetailModal').classList.add('hidden');
}
async function deleteUser(userId, email) {
if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/admin/users/${userId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('User deleted successfully', 'success');
loadUsers(); // Refresh the table
} else {
showToast('Failed to delete user', 'error');
}
} catch (error) {
console.error('Delete user error:', error);
showToast('Failed to delete user', 'error');
}
}
async function loadDatabaseSchema() {
try {
const response = await fetch('/admin/database/schema');
const data = await response.json();
if (data.success) {
updateDatabaseSchema(data.schema);
}
} catch (error) {
console.error('Schema loading error:', error);
showToast('Failed to load database schema', 'error');
}
}
function updateDatabaseSchema(schema) {
const container = document.getElementById('databaseSchema');
container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
<div class="border rounded-lg p-4">
<div class="flex justify-between items-center mb-3">
<h5 class="font-semibold text-lg">${tableName}</h5>
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
${tableInfo.row_count} rows
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
${tableInfo.columns.map(col => `
<div class="flex items-center space-x-2">
<span class="font-medium">${col.name}</span>
<span class="text-gray-500">${col.type}</span>
${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
</div>
`).join('')}
</div>
</div>
`).join('');
}
async function loadTokenUsage() {
try {
// For now, create sample charts with existing data
await createTokenUsageCharts();
} catch (error) {
console.error('Token usage loading error:', error);
showToast('Failed to load token usage data', 'error');
}
}
async function createTokenUsageCharts() {
// Provider Usage Chart
const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
new Chart(providerCtx, {
type: 'bar',
data: {
labels: ['Groq', 'Gemini'],
datasets: [{
label: 'Input Tokens',
data: [150000, 85000],
backgroundColor: 'rgba(59, 130, 246, 0.8)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}, {
label: 'Output Tokens',
data: [45000, 28000],
backgroundColor: 'rgba(147, 51, 234, 0.8)',
borderColor: 'rgba(147, 51, 234, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Token Usage by Provider'
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Tokens'
}
}
}
}
});
// Cost Breakdown Chart
const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
new Chart(costCtx, {
type: 'pie',
data: {
labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
datasets: [{
data: [40, 25, 20, 15],
backgroundColor: [
'rgba(34, 197, 94, 0.8)',
'rgba(59, 130, 246, 0.8)',
'rgba(147, 51, 234, 0.8)',
'rgba(251, 146, 60, 0.8)'
],
borderColor: [
'rgba(34, 197, 94, 1)',
'rgba(59, 130, 246, 1)',
'rgba(147, 51, 234, 1)',
'rgba(251, 146, 60, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Cost Distribution by Operation Type'
},
legend: {
position: 'bottom'
}
}
}
});
}
function refreshUsers() {
currentPage = 1;
loadUsers();
}
function previousPage() {
if (currentPage > 1) {
currentPage--;
loadUsers();
}
}
function nextPage() {
if (usersData && currentPage < usersData.total_pages) {
currentPage++;
loadUsers();
}
}
function createCharts(stats) {
createUserGrowthChart(stats);
createTokenCostChart(stats);
}
function createUserGrowthChart(stats) {
const ctx = document.getElementById('userGrowthChart').getContext('2d');
// Generate sample data for last 30 days
const dates = [];
const userData = [];
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
dates.push(date.toLocaleDateString());
// Simulate growth data (would come from real analytics)
userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
}
new Chart(ctx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Total Users',
data: userData,
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'User Growth Over Time'
}
},
interaction: {
intersect: false,
},
scales: {
x: {
display: true,
title: {
display: true,
text: 'Date'
}
},
y: {
display: true,
title: {
display: true,
text: 'Users'
}
}
}
}
});
}
function createTokenCostChart(stats) {
const ctx = document.getElementById('tokenCostChart').getContext('2d');
// Sample cost data
const providers = ['Groq', 'Gemini'];
const costs = [
(stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
(stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
];
new Chart(ctx, {
type: 'doughnut',
data: {
labels: providers,
datasets: [{
label: 'Cost ($)',
data: costs,
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(147, 51, 234, 0.8)'
],
borderColor: [
'rgba(59, 130, 246, 1)',
'rgba(147, 51, 234, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'API Costs by Provider'
},
legend: {
position: 'bottom',
}
}
}
});
}
function exportUsers() {
window.open('/admin/export/users', '_blank');
showToast('Users data export started', 'success');
}
function exportTokens() {
window.open('/admin/export/tokens', '_blank');
showToast('Token usage data export started', 'success');
}
async function loadSystemHealth() {
try {
// Load system health metrics
const healthResponse = await fetch('/admin/system/health');
const healthData = await healthResponse.json();
if (healthData.success) {
updateSystemMetrics(healthData.health);
}
// Load system alerts
const alertsResponse = await fetch('/admin/system/alerts');
const alertsData = await alertsResponse.json();
if (alertsData.success) {
updateSystemAlerts(alertsData.alerts);
}
} catch (error) {
console.error('System health loading error:', error);
showToast('Failed to load system health data', 'error');
}
}
function updateSystemMetrics(health) {
// Memory metrics
if (health.memory) {
const memoryPercent = health.memory.percent || 0;
const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
document.getElementById('memoryMetrics').innerHTML = `
<div class="space-y-2">
<div class="flex justify-between">
<span>Used:</span>
<span>${memoryUsed}GB / ${memoryTotal}GB</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
</div>
<div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
</div>
`;
}
// Disk metrics
if (health.disk) {
const diskPercent = health.disk.percent || 0;
const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
document.getElementById('diskMetrics').innerHTML = `
<div class="space-y-2">
<div class="flex justify-between">
<span>Used:</span>
<span>${diskUsed}GB / ${diskTotal}GB</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
</div>
<div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
</div>
`;
}
// Database metrics
if (health.database) {
document.getElementById('databaseMetrics').innerHTML = `
<div class="space-y-2">
<div class="flex justify-between">
<span>Size:</span>
<span>${health.database.size_mb}MB</span>
</div>
<div class="flex justify-between">
<span>Uptime:</span>
<span>${health.uptime || 'Unknown'}</span>
</div>
</div>
`;
}
// Error logs
if (health.recent_errors && health.recent_errors.length > 0) {
document.getElementById('errorLogs').innerHTML = `
<div class="space-y-2 max-h-64 overflow-y-auto">
${health.recent_errors.map(error => `
<div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
<div class="flex justify-between items-start">
<span class="font-medium text-red-800">${error.level}</span>
<span class="text-red-600 text-xs">${error.timestamp}</span>
</div>
<div class="text-red-700 mt-1">${error.message}</div>
<div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
</div>
`).join('')}
</div>
`;
} else {
document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
}
}
function updateSystemAlerts(alerts) {
const container = document.getElementById('systemAlerts');
if (alerts.length === 0) {
container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
return;
}
container.innerHTML = alerts.map(alert => {
const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
'bg-blue-50 border-blue-200';
const textColor = alert.type === 'error' ? 'text-red-800' :
alert.type === 'warning' ? 'text-yellow-800' :
'text-blue-800';
const icon = alert.type === 'error' ? '🚨' :
alert.type === 'warning' ? '⚠️' : 'ℹ️';
return `
<div class="p-3 ${bgColor} border rounded-lg">
<div class="flex items-start gap-3">
<span class="text-lg">${icon}</span>
<div class="flex-1">
<div class="font-medium ${textColor}">${alert.message}</div>
<div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
</div>
</div>
</div>
`;
}).join('');
}
function formatDate(dateString) {
if (!dateString) return 'Never';
return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' :
type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
} text-white`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 5000);
}
// Add CSS for tab styling
const style = document.createElement('style');
style.textContent = `
.admin-tab {
padding: 0.5rem 1rem;
border-bottom: 2px solid transparent;
font-medium: 500;
text-decoration: none;
transition: all 0.2s;
}
.admin-tab:hover {
text-decoration: none;
border-bottom-color: #d1d5db;
}
.admin-tab.active {
border-bottom-color: #3b82f6;
color: #3b82f6;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>