{% extends "base.html" %} {% block title %}Admin - English Helper{% endblock %} {% block content %}

🎓 English Helper Admin

{% 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 = 'No users found'; return; } tbody.innerHTML = users.map(user => `
${user.email}
ID: ${user.id}
${formatDate(user.created_at)}
${user.session_count} sessions
${user.flashcard_count} cards, ${user.article_count} articles
${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
`).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 = `

📋 Basic Information

Email: ${user.user.email}
ID: ${user.user.id}
Created: ${formatDate(user.user.created_at)}
Last Login: ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}
Email Confirmed: ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}

âš™ī¸ Settings

${Object.entries(user.settings).map(([key, value]) => `
${key}: ${value}
` ).join('')}

📊 Recent Activity

${user.recent_sessions.map(session => `
${session.activity} ${formatDate(session.timestamp)}
`).join('')}

đŸŽ¯ Token Usage

${user.token_usage.map(usage => `
${usage.provider}
Input: ${usage.input_tokens.toLocaleString()}
Output: ${usage.output_tokens.toLocaleString()}
Calls: ${usage.calls}
`).join('')}
`; 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]) => `
${tableName}
${tableInfo.row_count} rows
${tableInfo.columns.map(col => `
${col.name} ${col.type} ${col.primary_key ? 'PK' : ''} ${col.not_null ? 'NOT NULL' : ''}
`).join('')}
`).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 = `
Used: ${memoryUsed}GB / ${memoryTotal}GB
${memoryPercent.toFixed(1)}% used
`; } // 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 = `
Used: ${diskUsed}GB / ${diskTotal}GB
${diskPercent.toFixed(1)}% used
`; } // Database metrics if (health.database) { document.getElementById('databaseMetrics').innerHTML = `
Size: ${health.database.size_mb}MB
Uptime: ${health.uptime || 'Unknown'}
`; } // Error logs if (health.recent_errors && health.recent_errors.length > 0) { document.getElementById('errorLogs').innerHTML = `
${health.recent_errors.map(error => `
${error.level} ${error.timestamp}
${error.message}
Module: ${error.module}
`).join('')}
`; } else { document.getElementById('errorLogs').innerHTML = '
No recent errors
'; } } function updateSystemAlerts(alerts) { const container = document.getElementById('systemAlerts'); if (alerts.length === 0) { container.innerHTML = '
✅ All systems normal
'; 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 `
${icon}
${alert.message}
Action: ${alert.action}
`; }).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);