amauricunha's picture
Upload 4 files
041c8b9 verified
Raw
History Blame
23 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% block title %}English Helper{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<style>
/* Small helper styles for the header/navigation */
.site-nav { background: #ffffff; border-bottom: 1px solid #e5e7eb; }
.site-nav a { margin-right: 1rem; color: #374151; text-decoration: none; font-weight: 600; }
.site-nav a:hover { color: #1d4ed8; }
.container { max-width: 1100px; margin: 0 auto; padding: 1rem; }
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<header class="site-nav">
<div class="container flex items-center justify-between">
<div class="flex items-center gap-4">
<a href="/" class="flex items-center gap-2"><span style="font-size:20px">🎓</span><strong>English Helper</strong></a>
<nav class="hidden md:flex items-center">
<a href="/">Home</a>
<a href="/admin">Admin</a>
<a href="/status">Status</a>
</nav>
</div>
<div>
<!-- optional right-side links -->
</div>
</div>
</header>
<!-- Demo mode banner -->
<div id="demoBanner" class="container mt-2">
<div class="bg-yellow-50 border border-yellow-200 rounded p-3 text-sm flex justify-between items-center" style="display:none;" role="status">
<div>
<strong>Demo Mode</strong> — Running with in-memory data. Changes won't persist after a restart.
</div>
<div>
<button id="dismissDemo" class="px-3 py-1 bg-yellow-200 rounded text-sm">Dismiss</button>
</div>
</div>
</div>
<main class="container mt-6">
{% block content %}{% endblock %}
</main>
<!-- Place for page-specific scripts -->
{% block scripts %}{% endblock %}
<script>
(async function(){
try {
const dismissed = localStorage.getItem('eh_demo_dismissed');
if (dismissed === '1') return;
const res = await fetch('/system/status');
if (!res.ok) return;
const json = await res.json();
if (json.storage === 'in_memory' || json.demo_mode) {
const banner = document.getElementById('demoBanner').querySelector('div');
if (banner) {
banner.style.display = 'flex';
document.getElementById('dismissDemo').addEventListener('click', () => {
banner.style.display = 'none';
localStorage.setItem('eh_demo_dismissed', '1');
});
}
}
} catch (e) {
// ignore
}
})();
</script>
</body>
</html>
</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>