amauricunha's picture
Upload admin.html
879d451 verified
Raw
History Blame
30.3 kB
{% extends "base.html" %}
{% block title %}Admin - English Helper{% endblock %}
{% block content %}
<style>
.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>
<div id="adminInterface">
<!-- 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>
</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">
<!-- ...existing dashboard content... -->
</div>
<!-- Users Tab -->
<div id="usersTab" class="tab-content hidden">
<!-- ...existing users content... -->
</div>
<!-- Database Tab -->
<div id="databaseTab" class="tab-content hidden">
<!-- ...existing database content... -->
</div>
<!-- Token Usage Tab -->
<div id="tokensTab" class="tab-content hidden">
<!-- ...existing token usage content... -->
</div>
<!-- System Health Tab -->
<div id="systemTab" class="tab-content hidden">
<!-- ...existing system health content... -->
</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">
<!-- ...existing modal header... -->
</div>
<div id="userDetailContent">
<!-- ...existing modal content... -->
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// ...existing admin JS code...
</script>
{% endblock %}
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>