amauricunha commited on
Commit
e5fe13e
·
verified ·
1 Parent(s): f2c82d4

Upload 3 files

Browse files
Files changed (3) hide show
  1. templates/admin.html +899 -899
  2. templates/base.html +899 -899
  3. templates/index.html +0 -0
templates/admin.html CHANGED
@@ -1,900 +1,900 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>English Helper - Admin Panel</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
- <style>
10
- /* Tailwind utility classes should be used directly in HTML, not via @apply in <style> */
11
- .spinner {
12
- border: 3px solid #f3f3f3;
13
- border-top: 3px solid #3498db;
14
- border-radius: 50%;
15
- width: 20px;
16
- height: 20px;
17
- animation: spin 1s linear infinite;
18
- display: inline-block;
19
- margin-right: 10px;
20
- }
21
- @keyframes spin {
22
- 0% { transform: rotate(0deg); }
23
- 100% { transform: rotate(360deg); }
24
- }
25
- .table-container {
26
- max-height: 600px;
27
- overflow-y: auto;
28
- }
29
- </style>
30
- </head>
31
- <body class="bg-gray-100 min-h-screen">
32
- <!-- Admin Login Modal removed: admin panel is always accessible -->
33
-
34
- <!-- Admin Interface -->
35
- <div id="adminInterface">
36
- <!-- Header -->
37
- <header class="bg-white shadow-md">
38
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
39
- <div class="flex justify-between items-center py-4">
40
- <div class="flex items-center">
41
- <h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
42
- </div>
43
- <!-- Admin user info and logout removed -->
44
- </div>
45
- </div>
46
- </header>
47
-
48
- <!-- Navigation Tabs -->
49
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
50
- <div class="border-b border-gray-200">
51
- <nav class="-mb-px flex space-x-8">
52
- <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
53
- 📊 Dashboard
54
- </button>
55
- <button onclick="showTab('users')" class="admin-tab" data-tab="users">
56
- 👥 Users
57
- </button>
58
- <button onclick="showTab('database')" class="admin-tab" data-tab="database">
59
- 🗄️ Database
60
- </button>
61
- <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
62
- 🎯 Token Usage
63
- </button>
64
- <button onclick="showTab('system')" class="admin-tab" data-tab="system">
65
- 🖥️ System Health
66
- </button>
67
- </nav>
68
- </div>
69
- </div>
70
-
71
- <!-- Tab Content -->
72
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
73
- <!-- Dashboard Tab -->
74
- <div id="dashboardTab" class="tab-content">
75
- <h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
76
-
77
- <!-- System Stats Cards -->
78
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
79
- <div class="stat-card">
80
- <h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
81
- <p id="totalUsers" class="text-3xl font-bold">-</p>
82
- <p class="text-sm opacity-80">
83
- <span id="newUsersWeek">-</span> this week
84
- </p>
85
- </div>
86
- <div class="stat-card">
87
- <h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
88
- <p id="totalSessions" class="text-3xl font-bold">-</p>
89
- <p class="text-sm opacity-80">All time</p>
90
- </div>
91
- <div class="stat-card">
92
- <h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
93
- <p id="totalTokens" class="text-3xl font-bold">-</p>
94
- <p class="text-sm opacity-80">
95
- $<span id="estimatedCost">-</span> estimated cost
96
- </p>
97
- </div>
98
- <div class="stat-card">
99
- <h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
100
- <p id="totalContent" class="text-3xl font-bold">-</p>
101
- <p class="text-sm opacity-80">Articles & flashcards</p>
102
- </div>
103
- </div>
104
-
105
- <!-- Charts Section -->
106
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
107
- <div class="admin-card">
108
- <h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
109
- <canvas id="userGrowthChart" width="400" height="200"></canvas>
110
- </div>
111
- <div class="admin-card">
112
- <h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
113
- <canvas id="tokenCostChart" width="400" height="200"></canvas>
114
- </div>
115
- </div>
116
-
117
- <!-- Recent Activity -->
118
- <div class="admin-card">
119
- <h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
120
- <div id="recentActivity" class="space-y-2">
121
- <div class="text-center text-gray-500 py-4">Loading recent activity...</div>
122
- </div>
123
- </div>
124
- </div>
125
-
126
- <!-- Users Tab -->
127
- <!-- Users Tab removed: user management is not available in open version -->
128
-
129
- <!-- Database Tab -->
130
- <div id="databaseTab" class="tab-content hidden">
131
- <h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
132
-
133
- <div class="admin-card">
134
- <h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
135
- <div id="databaseSchema" class="space-y-4">
136
- <div class="text-center text-gray-500 py-4">Loading schema...</div>
137
- </div>
138
- </div>
139
- </div>
140
-
141
- <!-- Token Usage Tab -->
142
- <div id="tokensTab" class="tab-content hidden">
143
- <div class="flex justify-between items-center mb-6">
144
- <h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
145
- <button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
146
- 📥 Export Token Data
147
- </button>
148
- </div>
149
-
150
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
151
- <div class="admin-card">
152
- <h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
153
- <canvas id="providerUsageChart" width="400" height="300"></canvas>
154
- </div>
155
- <div class="admin-card">
156
- <h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
157
- <canvas id="costBreakdownChart" width="400" height="300"></canvas>
158
- </div>
159
- </div>
160
- </div>
161
-
162
- <!-- System Health Tab -->
163
- <div id="systemTab" class="tab-content hidden">
164
- <h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
165
-
166
- <!-- System Alerts -->
167
- <div class="admin-card mb-6">
168
- <h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
169
- <div id="systemAlerts">
170
- <div class="text-center text-gray-500 py-4">Loading alerts...</div>
171
- </div>
172
- </div>
173
-
174
- <!-- System Metrics -->
175
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
176
- <div class="admin-card">
177
- <h4 class="font-semibold mb-3">💾 Memory Usage</h4>
178
- <div id="memoryMetrics">
179
- <div class="text-sm text-gray-500">Loading...</div>
180
- </div>
181
- </div>
182
-
183
- <div class="admin-card">
184
- <h4 class="font-semibold mb-3">💿 Disk Usage</h4>
185
- <div id="diskMetrics">
186
- <div class="text-sm text-gray-500">Loading...</div>
187
- </div>
188
- </div>
189
-
190
- <div class="admin-card">
191
- <h4 class="font-semibold mb-3">🗄️ Database</h4>
192
- <div id="databaseMetrics">
193
- <div class="text-sm text-gray-500">Loading...</div>
194
- </div>
195
- </div>
196
- </div>
197
-
198
- <!-- Error Logs -->
199
- <div class="admin-card">
200
- <h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
201
- <div id="errorLogs">
202
- <div class="text-center text-gray-500 py-4">Loading error logs...</div>
203
- </div>
204
- </div>
205
- </div>
206
- </div>
207
- </div>
208
-
209
- <!-- User Detail Modal removed -->
210
-
211
- <!-- Toast Notifications -->
212
- <div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
213
-
214
- <script>
215
- // User logic, authentication, and user management removed. Admin panel is always accessible.
216
-
217
- // Tab management (users tab removed)
218
- function showTab(tabName) {
219
- document.querySelectorAll('.tab-content').forEach(tab => {
220
- tab.classList.add('hidden');
221
- });
222
- document.querySelectorAll('.admin-tab').forEach(btn => {
223
- btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
224
- btn.classList.add('border-transparent', 'text-gray-500');
225
- });
226
- document.getElementById(tabName + 'Tab').classList.remove('hidden');
227
- const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
228
- activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
229
- activeBtn.classList.remove('border-transparent', 'text-gray-500');
230
- if (tabName === 'dashboard') {
231
- loadDashboard();
232
- } else if (tabName === 'database') {
233
- loadDatabaseSchema();
234
- } else if (tabName === 'tokens') {
235
- loadTokenUsage();
236
- } else if (tabName === 'system') {
237
- loadSystemHealth();
238
- }
239
- }
240
-
241
- async function loadDashboard() {
242
- try {
243
- const response = await fetch('/admin/dashboard');
244
- const data = await response.json();
245
-
246
- if (data.success) {
247
- const stats = data.stats;
248
-
249
- // Update stat cards
250
- document.getElementById('totalUsers').textContent = stats.users?.total || 0;
251
- document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
252
- document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
253
- document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
254
- document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
255
- document.getElementById('totalContent').textContent =
256
- (stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
257
-
258
- // Update recent activity
259
- updateRecentActivity(stats.recent_activity || []);
260
-
261
- // Create charts
262
- createCharts(stats);
263
- }
264
- } catch (error) {
265
- console.error('Dashboard loading error:', error);
266
- showToast('Failed to load dashboard', 'error');
267
- }
268
- }
269
-
270
- function updateRecentActivity(activities) {
271
- const container = document.getElementById('recentActivity');
272
-
273
- if (activities.length === 0) {
274
- container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
275
- return;
276
- }
277
-
278
- container.innerHTML = activities.map(activity => `
279
- <div class="flex items-center justify-between py-2 border-b border-gray-100">
280
- <div>
281
- <span class="font-medium">${activity.user}</span>
282
- <span class="text-gray-600">performed ${activity.activity}</span>
283
- </div>
284
- <span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
285
- </div>
286
- `).join('');
287
- }
288
-
289
- async function loadUsers() {
290
- try {
291
- const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
292
- const data = await response.json();
293
-
294
- if (data.success) {
295
- usersData = data.data;
296
- updateUsersTable(usersData.users);
297
- updateUsersPagination(usersData);
298
- }
299
- } catch (error) {
300
- console.error('Users loading error:', error);
301
- showToast('Failed to load users', 'error');
302
- }
303
- }
304
-
305
- function updateUsersTable(users) {
306
- const tbody = document.getElementById('usersTableBody');
307
-
308
- if (users.length === 0) {
309
- tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
310
- return;
311
- }
312
-
313
- tbody.innerHTML = users.map(user => `
314
- <tr class="border-b hover:bg-gray-50">
315
- <td class="px-4 py-3">
316
- <div>
317
- <div class="font-medium">${user.email}</div>
318
- <div class="text-sm text-gray-500">ID: ${user.id}</div>
319
- </div>
320
- </td>
321
- <td class="px-4 py-3 text-sm text-gray-600">
322
- ${formatDate(user.created_at)}
323
- </td>
324
- <td class="px-4 py-3">
325
- <div class="text-sm">
326
- <div>${user.session_count} sessions</div>
327
- <div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
328
- </div>
329
- </td>
330
- <td class="px-4 py-3">
331
- <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'}">
332
- ${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
333
- </span>
334
- </td>
335
- <td class="px-4 py-3">
336
- <div class="flex space-x-2">
337
- <button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
338
- <button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
339
- </div>
340
- </td>
341
- </tr>
342
- `).join('');
343
- }
344
-
345
- function updateUsersPagination(data) {
346
- document.getElementById('usersInfo').textContent =
347
- `Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
348
-
349
- document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
350
-
351
- document.getElementById('prevPageBtn').disabled = data.page <= 1;
352
- document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
353
- }
354
-
355
- async function viewUser(userId) {
356
- try {
357
- const response = await fetch(`/admin/users/${userId}`);
358
- const data = await response.json();
359
-
360
- if (data.success) {
361
- showUserModal(data.user);
362
- }
363
- } catch (error) {
364
- console.error('User details error:', error);
365
- showToast('Failed to load user details', 'error');
366
- }
367
- }
368
-
369
- function showUserModal(user) {
370
- const modal = document.getElementById('userDetailModal');
371
- const content = document.getElementById('userDetailContent');
372
-
373
- content.innerHTML = `
374
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
- <div>
376
- <h4 class="font-semibold mb-3">📋 Basic Information</h4>
377
- <div class="space-y-2 text-sm">
378
- <div><strong>Email:</strong> ${user.user.email}</div>
379
- <div><strong>ID:</strong> ${user.user.id}</div>
380
- <div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
381
- <div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
382
- <div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
383
- </div>
384
- </div>
385
-
386
- <div>
387
- <h4 class="font-semibold mb-3">⚙️ Settings</h4>
388
- <div class="space-y-2 text-sm">
389
- ${Object.entries(user.settings).map(([key, value]) =>
390
- `<div><strong>${key}:</strong> ${value}</div>`
391
- ).join('')}
392
- </div>
393
- </div>
394
- </div>
395
-
396
- <div class="mt-6">
397
- <h4 class="font-semibold mb-3">📊 Recent Activity</h4>
398
- <div class="max-h-48 overflow-y-auto">
399
- ${user.recent_sessions.map(session => `
400
- <div class="flex justify-between py-2 border-b text-sm">
401
- <span>${session.activity}</span>
402
- <span class="text-gray-500">${formatDate(session.timestamp)}</span>
403
- </div>
404
- `).join('')}
405
- </div>
406
- </div>
407
-
408
- <div class="mt-6">
409
- <h4 class="font-semibold mb-3">🎯 Token Usage</h4>
410
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
411
- ${user.token_usage.map(usage => `
412
- <div class="bg-gray-50 p-3 rounded">
413
- <div class="font-medium">${usage.provider}</div>
414
- <div class="text-sm text-gray-600">
415
- Input: ${usage.input_tokens.toLocaleString()}<br>
416
- Output: ${usage.output_tokens.toLocaleString()}<br>
417
- Calls: ${usage.calls}
418
- </div>
419
- </div>
420
- `).join('')}
421
- </div>
422
- </div>
423
- `;
424
-
425
- modal.classList.remove('hidden');
426
- }
427
-
428
- function closeUserModal() {
429
- document.getElementById('userDetailModal').classList.add('hidden');
430
- }
431
-
432
- async function deleteUser(userId, email) {
433
- if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
434
- return;
435
- }
436
-
437
- try {
438
- const response = await fetch(`/admin/users/${userId}`, {
439
- method: 'DELETE'
440
- });
441
-
442
- const data = await response.json();
443
-
444
- if (data.success) {
445
- showToast('User deleted successfully', 'success');
446
- loadUsers(); // Refresh the table
447
- } else {
448
- showToast('Failed to delete user', 'error');
449
- }
450
- } catch (error) {
451
- console.error('Delete user error:', error);
452
- showToast('Failed to delete user', 'error');
453
- }
454
- }
455
-
456
- async function loadDatabaseSchema() {
457
- try {
458
- const response = await fetch('/admin/database/schema');
459
- const data = await response.json();
460
-
461
- if (data.success) {
462
- updateDatabaseSchema(data.schema);
463
- }
464
- } catch (error) {
465
- console.error('Schema loading error:', error);
466
- showToast('Failed to load database schema', 'error');
467
- }
468
- }
469
-
470
- function updateDatabaseSchema(schema) {
471
- const container = document.getElementById('databaseSchema');
472
-
473
- container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
474
- <div class="border rounded-lg p-4">
475
- <div class="flex justify-between items-center mb-3">
476
- <h5 class="font-semibold text-lg">${tableName}</h5>
477
- <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
478
- ${tableInfo.row_count} rows
479
- </span>
480
- </div>
481
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
482
- ${tableInfo.columns.map(col => `
483
- <div class="flex items-center space-x-2">
484
- <span class="font-medium">${col.name}</span>
485
- <span class="text-gray-500">${col.type}</span>
486
- ${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
487
- ${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
488
- </div>
489
- `).join('')}
490
- </div>
491
- </div>
492
- `).join('');
493
- }
494
-
495
- async function loadTokenUsage() {
496
- try {
497
- // For now, create sample charts with existing data
498
- await createTokenUsageCharts();
499
- } catch (error) {
500
- console.error('Token usage loading error:', error);
501
- showToast('Failed to load token usage data', 'error');
502
- }
503
- }
504
-
505
- async function createTokenUsageCharts() {
506
- // Provider Usage Chart
507
- const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
508
-
509
- new Chart(providerCtx, {
510
- type: 'bar',
511
- data: {
512
- labels: ['Groq', 'Gemini'],
513
- datasets: [{
514
- label: 'Input Tokens',
515
- data: [150000, 85000],
516
- backgroundColor: 'rgba(59, 130, 246, 0.8)',
517
- borderColor: 'rgba(59, 130, 246, 1)',
518
- borderWidth: 1
519
- }, {
520
- label: 'Output Tokens',
521
- data: [45000, 28000],
522
- backgroundColor: 'rgba(147, 51, 234, 0.8)',
523
- borderColor: 'rgba(147, 51, 234, 1)',
524
- borderWidth: 1
525
- }]
526
- },
527
- options: {
528
- responsive: true,
529
- plugins: {
530
- title: {
531
- display: true,
532
- text: 'Token Usage by Provider'
533
- }
534
- },
535
- scales: {
536
- y: {
537
- beginAtZero: true,
538
- title: {
539
- display: true,
540
- text: 'Tokens'
541
- }
542
- }
543
- }
544
- }
545
- });
546
-
547
- // Cost Breakdown Chart
548
- const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
549
-
550
- new Chart(costCtx, {
551
- type: 'pie',
552
- data: {
553
- labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
554
- datasets: [{
555
- data: [40, 25, 20, 15],
556
- backgroundColor: [
557
- 'rgba(34, 197, 94, 0.8)',
558
- 'rgba(59, 130, 246, 0.8)',
559
- 'rgba(147, 51, 234, 0.8)',
560
- 'rgba(251, 146, 60, 0.8)'
561
- ],
562
- borderColor: [
563
- 'rgba(34, 197, 94, 1)',
564
- 'rgba(59, 130, 246, 1)',
565
- 'rgba(147, 51, 234, 1)',
566
- 'rgba(251, 146, 60, 1)'
567
- ],
568
- borderWidth: 2
569
- }]
570
- },
571
- options: {
572
- responsive: true,
573
- plugins: {
574
- title: {
575
- display: true,
576
- text: 'Cost Distribution by Operation Type'
577
- },
578
- legend: {
579
- position: 'bottom'
580
- }
581
- }
582
- }
583
- });
584
- }
585
-
586
- function refreshUsers() {
587
- currentPage = 1;
588
- loadUsers();
589
- }
590
-
591
- function previousPage() {
592
- if (currentPage > 1) {
593
- currentPage--;
594
- loadUsers();
595
- }
596
- }
597
-
598
- function nextPage() {
599
- if (usersData && currentPage < usersData.total_pages) {
600
- currentPage++;
601
- loadUsers();
602
- }
603
- }
604
-
605
- function createCharts(stats) {
606
- createUserGrowthChart(stats);
607
- createTokenCostChart(stats);
608
- }
609
-
610
- function createUserGrowthChart(stats) {
611
- const ctx = document.getElementById('userGrowthChart').getContext('2d');
612
-
613
- // Generate sample data for last 30 days
614
- const dates = [];
615
- const userData = [];
616
- const today = new Date();
617
-
618
- for (let i = 29; i >= 0; i--) {
619
- const date = new Date(today);
620
- date.setDate(date.getDate() - i);
621
- dates.push(date.toLocaleDateString());
622
-
623
- // Simulate growth data (would come from real analytics)
624
- userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
625
- }
626
-
627
- new Chart(ctx, {
628
- type: 'line',
629
- data: {
630
- labels: dates,
631
- datasets: [{
632
- label: 'Total Users',
633
- data: userData,
634
- borderColor: 'rgb(59, 130, 246)',
635
- backgroundColor: 'rgba(59, 130, 246, 0.1)',
636
- fill: true,
637
- tension: 0.4
638
- }]
639
- },
640
- options: {
641
- responsive: true,
642
- plugins: {
643
- title: {
644
- display: true,
645
- text: 'User Growth Over Time'
646
- }
647
- },
648
- interaction: {
649
- intersect: false,
650
- },
651
- scales: {
652
- x: {
653
- display: true,
654
- title: {
655
- display: true,
656
- text: 'Date'
657
- }
658
- },
659
- y: {
660
- display: true,
661
- title: {
662
- display: true,
663
- text: 'Users'
664
- }
665
- }
666
- }
667
- }
668
- });
669
- }
670
-
671
- function createTokenCostChart(stats) {
672
- const ctx = document.getElementById('tokenCostChart').getContext('2d');
673
-
674
- // Sample cost data
675
- const providers = ['Groq', 'Gemini'];
676
- const costs = [
677
- (stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
678
- (stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
679
- ];
680
-
681
- new Chart(ctx, {
682
- type: 'doughnut',
683
- data: {
684
- labels: providers,
685
- datasets: [{
686
- label: 'Cost ($)',
687
- data: costs,
688
- backgroundColor: [
689
- 'rgba(59, 130, 246, 0.8)',
690
- 'rgba(147, 51, 234, 0.8)'
691
- ],
692
- borderColor: [
693
- 'rgba(59, 130, 246, 1)',
694
- 'rgba(147, 51, 234, 1)'
695
- ],
696
- borderWidth: 2
697
- }]
698
- },
699
- options: {
700
- responsive: true,
701
- plugins: {
702
- title: {
703
- display: true,
704
- text: 'API Costs by Provider'
705
- },
706
- legend: {
707
- position: 'bottom',
708
- }
709
- }
710
- }
711
- });
712
- }
713
-
714
- function exportUsers() {
715
- window.open('/admin/export/users', '_blank');
716
- showToast('Users data export started', 'success');
717
- }
718
-
719
- function exportTokens() {
720
- window.open('/admin/export/tokens', '_blank');
721
- showToast('Token usage data export started', 'success');
722
- }
723
-
724
- async function loadSystemHealth() {
725
- try {
726
- // Load system health metrics
727
- const healthResponse = await fetch('/admin/system/health');
728
- const healthData = await healthResponse.json();
729
-
730
- if (healthData.success) {
731
- updateSystemMetrics(healthData.health);
732
- }
733
-
734
- // Load system alerts
735
- const alertsResponse = await fetch('/admin/system/alerts');
736
- const alertsData = await alertsResponse.json();
737
-
738
- if (alertsData.success) {
739
- updateSystemAlerts(alertsData.alerts);
740
- }
741
- } catch (error) {
742
- console.error('System health loading error:', error);
743
- showToast('Failed to load system health data', 'error');
744
- }
745
- }
746
-
747
- function updateSystemMetrics(health) {
748
- // Memory metrics
749
- if (health.memory) {
750
- const memoryPercent = health.memory.percent || 0;
751
- const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
752
- const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
753
-
754
- document.getElementById('memoryMetrics').innerHTML = `
755
- <div class="space-y-2">
756
- <div class="flex justify-between">
757
- <span>Used:</span>
758
- <span>${memoryUsed}GB / ${memoryTotal}GB</span>
759
- </div>
760
- <div class="w-full bg-gray-200 rounded-full h-2">
761
- <div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
762
- </div>
763
- <div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
764
- </div>
765
- `;
766
- }
767
-
768
- // Disk metrics
769
- if (health.disk) {
770
- const diskPercent = health.disk.percent || 0;
771
- const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
772
- const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
773
-
774
- document.getElementById('diskMetrics').innerHTML = `
775
- <div class="space-y-2">
776
- <div class="flex justify-between">
777
- <span>Used:</span>
778
- <span>${diskUsed}GB / ${diskTotal}GB</span>
779
- </div>
780
- <div class="w-full bg-gray-200 rounded-full h-2">
781
- <div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
782
- </div>
783
- <div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
784
- </div>
785
- `;
786
- }
787
-
788
- // Database metrics
789
- if (health.database) {
790
- document.getElementById('databaseMetrics').innerHTML = `
791
- <div class="space-y-2">
792
- <div class="flex justify-between">
793
- <span>Size:</span>
794
- <span>${health.database.size_mb}MB</span>
795
- </div>
796
- <div class="flex justify-between">
797
- <span>Uptime:</span>
798
- <span>${health.uptime || 'Unknown'}</span>
799
- </div>
800
- </div>
801
- `;
802
- }
803
-
804
- // Error logs
805
- if (health.recent_errors && health.recent_errors.length > 0) {
806
- document.getElementById('errorLogs').innerHTML = `
807
- <div class="space-y-2 max-h-64 overflow-y-auto">
808
- ${health.recent_errors.map(error => `
809
- <div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
810
- <div class="flex justify-between items-start">
811
- <span class="font-medium text-red-800">${error.level}</span>
812
- <span class="text-red-600 text-xs">${error.timestamp}</span>
813
- </div>
814
- <div class="text-red-700 mt-1">${error.message}</div>
815
- <div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
816
- </div>
817
- `).join('')}
818
- </div>
819
- `;
820
- } else {
821
- document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
822
- }
823
- }
824
-
825
- function updateSystemAlerts(alerts) {
826
- const container = document.getElementById('systemAlerts');
827
-
828
- if (alerts.length === 0) {
829
- container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
830
- return;
831
- }
832
-
833
- container.innerHTML = alerts.map(alert => {
834
- const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
835
- alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
836
- 'bg-blue-50 border-blue-200';
837
- const textColor = alert.type === 'error' ? 'text-red-800' :
838
- alert.type === 'warning' ? 'text-yellow-800' :
839
- 'text-blue-800';
840
- const icon = alert.type === 'error' ? '🚨' :
841
- alert.type === 'warning' ? '⚠️' : 'ℹ️';
842
-
843
- return `
844
- <div class="p-3 ${bgColor} border rounded-lg">
845
- <div class="flex items-start gap-3">
846
- <span class="text-lg">${icon}</span>
847
- <div class="flex-1">
848
- <div class="font-medium ${textColor}">${alert.message}</div>
849
- <div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
850
- </div>
851
- </div>
852
- </div>
853
- `;
854
- }).join('');
855
- }
856
-
857
- function formatDate(dateString) {
858
- if (!dateString) return 'Never';
859
- return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
860
- }
861
-
862
- function showToast(message, type = 'info') {
863
- const toast = document.createElement('div');
864
- toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
865
- type === 'success' ? 'bg-green-500' :
866
- type === 'error' ? 'bg-red-500' :
867
- type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
868
- } text-white`;
869
-
870
- toast.textContent = message;
871
- document.body.appendChild(toast);
872
-
873
- setTimeout(() => {
874
- toast.remove();
875
- }, 5000);
876
- }
877
-
878
- // Add CSS for tab styling
879
- const style = document.createElement('style');
880
- style.textContent = `
881
- .admin-tab {
882
- padding: 0.5rem 1rem;
883
- border-bottom: 2px solid transparent;
884
- font-medium: 500;
885
- text-decoration: none;
886
- transition: all 0.2s;
887
- }
888
- .admin-tab:hover {
889
- text-decoration: none;
890
- border-bottom-color: #d1d5db;
891
- }
892
- .admin-tab.active {
893
- border-bottom-color: #3b82f6;
894
- color: #3b82f6;
895
- }
896
- `;
897
- document.head.appendChild(style);
898
- </script>
899
- </body>
900
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>English Helper - Admin Panel</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ /* Tailwind utility classes should be used directly in HTML, not via @apply in <style> */
11
+ .spinner {
12
+ border: 3px solid #f3f3f3;
13
+ border-top: 3px solid #3498db;
14
+ border-radius: 50%;
15
+ width: 20px;
16
+ height: 20px;
17
+ animation: spin 1s linear infinite;
18
+ display: inline-block;
19
+ margin-right: 10px;
20
+ }
21
+ @keyframes spin {
22
+ 0% { transform: rotate(0deg); }
23
+ 100% { transform: rotate(360deg); }
24
+ }
25
+ .table-container {
26
+ max-height: 600px;
27
+ overflow-y: auto;
28
+ }
29
+ </style>
30
+ </head>
31
+ <body class="bg-gray-100 min-h-screen">
32
+ <!-- Admin Login Modal removed: admin panel is always accessible -->
33
+
34
+ <!-- Admin Interface -->
35
+ <div id="adminInterface">
36
+ <!-- Header -->
37
+ <header class="bg-white shadow-md">
38
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
39
+ <div class="flex justify-between items-center py-4">
40
+ <div class="flex items-center">
41
+ <h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
42
+ </div>
43
+ <!-- Admin user info and logout removed -->
44
+ </div>
45
+ </div>
46
+ </header>
47
+
48
+ <!-- Navigation Tabs -->
49
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
50
+ <div class="border-b border-gray-200">
51
+ <nav class="-mb-px flex space-x-8">
52
+ <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
53
+ 📊 Dashboard
54
+ </button>
55
+ <button onclick="showTab('users')" class="admin-tab" data-tab="users">
56
+ 👥 Users
57
+ </button>
58
+ <button onclick="showTab('database')" class="admin-tab" data-tab="database">
59
+ 🗄️ Database
60
+ </button>
61
+ <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
62
+ 🎯 Token Usage
63
+ </button>
64
+ <button onclick="showTab('system')" class="admin-tab" data-tab="system">
65
+ 🖥️ System Health
66
+ </button>
67
+ </nav>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Tab Content -->
72
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
73
+ <!-- Dashboard Tab -->
74
+ <div id="dashboardTab" class="tab-content">
75
+ <h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
76
+
77
+ <!-- System Stats Cards -->
78
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
79
+ <div class="stat-card">
80
+ <h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
81
+ <p id="totalUsers" class="text-3xl font-bold">-</p>
82
+ <p class="text-sm opacity-80">
83
+ <span id="newUsersWeek">-</span> this week
84
+ </p>
85
+ </div>
86
+ <div class="stat-card">
87
+ <h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
88
+ <p id="totalSessions" class="text-3xl font-bold">-</p>
89
+ <p class="text-sm opacity-80">All time</p>
90
+ </div>
91
+ <div class="stat-card">
92
+ <h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
93
+ <p id="totalTokens" class="text-3xl font-bold">-</p>
94
+ <p class="text-sm opacity-80">
95
+ $<span id="estimatedCost">-</span> estimated cost
96
+ </p>
97
+ </div>
98
+ <div class="stat-card">
99
+ <h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
100
+ <p id="totalContent" class="text-3xl font-bold">-</p>
101
+ <p class="text-sm opacity-80">Articles & flashcards</p>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Charts Section -->
106
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
107
+ <div class="admin-card">
108
+ <h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
109
+ <canvas id="userGrowthChart" width="400" height="200"></canvas>
110
+ </div>
111
+ <div class="admin-card">
112
+ <h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
113
+ <canvas id="tokenCostChart" width="400" height="200"></canvas>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Recent Activity -->
118
+ <div class="admin-card">
119
+ <h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
120
+ <div id="recentActivity" class="space-y-2">
121
+ <div class="text-center text-gray-500 py-4">Loading recent activity...</div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Users Tab -->
127
+ <!-- Users Tab removed: user management is not available in open version -->
128
+
129
+ <!-- Database Tab -->
130
+ <div id="databaseTab" class="tab-content hidden">
131
+ <h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
132
+
133
+ <div class="admin-card">
134
+ <h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
135
+ <div id="databaseSchema" class="space-y-4">
136
+ <div class="text-center text-gray-500 py-4">Loading schema...</div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Token Usage Tab -->
142
+ <div id="tokensTab" class="tab-content hidden">
143
+ <div class="flex justify-between items-center mb-6">
144
+ <h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
145
+ <button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
146
+ 📥 Export Token Data
147
+ </button>
148
+ </div>
149
+
150
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
151
+ <div class="admin-card">
152
+ <h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
153
+ <canvas id="providerUsageChart" width="400" height="300"></canvas>
154
+ </div>
155
+ <div class="admin-card">
156
+ <h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
157
+ <canvas id="costBreakdownChart" width="400" height="300"></canvas>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- System Health Tab -->
163
+ <div id="systemTab" class="tab-content hidden">
164
+ <h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
165
+
166
+ <!-- System Alerts -->
167
+ <div class="admin-card mb-6">
168
+ <h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
169
+ <div id="systemAlerts">
170
+ <div class="text-center text-gray-500 py-4">Loading alerts...</div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- System Metrics -->
175
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
176
+ <div class="admin-card">
177
+ <h4 class="font-semibold mb-3">💾 Memory Usage</h4>
178
+ <div id="memoryMetrics">
179
+ <div class="text-sm text-gray-500">Loading...</div>
180
+ </div>
181
+ </div>
182
+
183
+ <div class="admin-card">
184
+ <h4 class="font-semibold mb-3">💿 Disk Usage</h4>
185
+ <div id="diskMetrics">
186
+ <div class="text-sm text-gray-500">Loading...</div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="admin-card">
191
+ <h4 class="font-semibold mb-3">🗄️ Database</h4>
192
+ <div id="databaseMetrics">
193
+ <div class="text-sm text-gray-500">Loading...</div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Error Logs -->
199
+ <div class="admin-card">
200
+ <h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
201
+ <div id="errorLogs">
202
+ <div class="text-center text-gray-500 py-4">Loading error logs...</div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- User Detail Modal removed -->
210
+
211
+ <!-- Toast Notifications -->
212
+ <div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
213
+
214
+ <script>
215
+ // User logic, authentication, and user management removed. Admin panel is always accessible.
216
+
217
+ // Tab management (users tab removed)
218
+ function showTab(tabName) {
219
+ document.querySelectorAll('.tab-content').forEach(tab => {
220
+ tab.classList.add('hidden');
221
+ });
222
+ document.querySelectorAll('.admin-tab').forEach(btn => {
223
+ btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
224
+ btn.classList.add('border-transparent', 'text-gray-500');
225
+ });
226
+ document.getElementById(tabName + 'Tab').classList.remove('hidden');
227
+ const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
228
+ activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
229
+ activeBtn.classList.remove('border-transparent', 'text-gray-500');
230
+ if (tabName === 'dashboard') {
231
+ loadDashboard();
232
+ } else if (tabName === 'database') {
233
+ loadDatabaseSchema();
234
+ } else if (tabName === 'tokens') {
235
+ loadTokenUsage();
236
+ } else if (tabName === 'system') {
237
+ loadSystemHealth();
238
+ }
239
+ }
240
+
241
+ async function loadDashboard() {
242
+ try {
243
+ const response = await fetch('/admin/dashboard');
244
+ const data = await response.json();
245
+
246
+ if (data.success) {
247
+ const stats = data.stats;
248
+
249
+ // Update stat cards
250
+ document.getElementById('totalUsers').textContent = stats.users?.total || 0;
251
+ document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
252
+ document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
253
+ document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
254
+ document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
255
+ document.getElementById('totalContent').textContent =
256
+ (stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
257
+
258
+ // Update recent activity
259
+ updateRecentActivity(stats.recent_activity || []);
260
+
261
+ // Create charts
262
+ createCharts(stats);
263
+ }
264
+ } catch (error) {
265
+ console.error('Dashboard loading error:', error);
266
+ showToast('Failed to load dashboard', 'error');
267
+ }
268
+ }
269
+
270
+ function updateRecentActivity(activities) {
271
+ const container = document.getElementById('recentActivity');
272
+
273
+ if (activities.length === 0) {
274
+ container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
275
+ return;
276
+ }
277
+
278
+ container.innerHTML = activities.map(activity => `
279
+ <div class="flex items-center justify-between py-2 border-b border-gray-100">
280
+ <div>
281
+ <span class="font-medium">${activity.user}</span>
282
+ <span class="text-gray-600">performed ${activity.activity}</span>
283
+ </div>
284
+ <span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
285
+ </div>
286
+ `).join('');
287
+ }
288
+
289
+ async function loadUsers() {
290
+ try {
291
+ const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
292
+ const data = await response.json();
293
+
294
+ if (data.success) {
295
+ usersData = data.data;
296
+ updateUsersTable(usersData.users);
297
+ updateUsersPagination(usersData);
298
+ }
299
+ } catch (error) {
300
+ console.error('Users loading error:', error);
301
+ showToast('Failed to load users', 'error');
302
+ }
303
+ }
304
+
305
+ function updateUsersTable(users) {
306
+ const tbody = document.getElementById('usersTableBody');
307
+
308
+ if (users.length === 0) {
309
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
310
+ return;
311
+ }
312
+
313
+ tbody.innerHTML = users.map(user => `
314
+ <tr class="border-b hover:bg-gray-50">
315
+ <td class="px-4 py-3">
316
+ <div>
317
+ <div class="font-medium">${user.email}</div>
318
+ <div class="text-sm text-gray-500">ID: ${user.id}</div>
319
+ </div>
320
+ </td>
321
+ <td class="px-4 py-3 text-sm text-gray-600">
322
+ ${formatDate(user.created_at)}
323
+ </td>
324
+ <td class="px-4 py-3">
325
+ <div class="text-sm">
326
+ <div>${user.session_count} sessions</div>
327
+ <div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
328
+ </div>
329
+ </td>
330
+ <td class="px-4 py-3">
331
+ <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'}">
332
+ ${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
333
+ </span>
334
+ </td>
335
+ <td class="px-4 py-3">
336
+ <div class="flex space-x-2">
337
+ <button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
338
+ <button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
339
+ </div>
340
+ </td>
341
+ </tr>
342
+ `).join('');
343
+ }
344
+
345
+ function updateUsersPagination(data) {
346
+ document.getElementById('usersInfo').textContent =
347
+ `Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
348
+
349
+ document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
350
+
351
+ document.getElementById('prevPageBtn').disabled = data.page <= 1;
352
+ document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
353
+ }
354
+
355
+ async function viewUser(userId) {
356
+ try {
357
+ const response = await fetch(`/admin/users/${userId}`);
358
+ const data = await response.json();
359
+
360
+ if (data.success) {
361
+ showUserModal(data.user);
362
+ }
363
+ } catch (error) {
364
+ console.error('User details error:', error);
365
+ showToast('Failed to load user details', 'error');
366
+ }
367
+ }
368
+
369
+ function showUserModal(user) {
370
+ const modal = document.getElementById('userDetailModal');
371
+ const content = document.getElementById('userDetailContent');
372
+
373
+ content.innerHTML = `
374
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
+ <div>
376
+ <h4 class="font-semibold mb-3">📋 Basic Information</h4>
377
+ <div class="space-y-2 text-sm">
378
+ <div><strong>Email:</strong> ${user.user.email}</div>
379
+ <div><strong>ID:</strong> ${user.user.id}</div>
380
+ <div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
381
+ <div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
382
+ <div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
383
+ </div>
384
+ </div>
385
+
386
+ <div>
387
+ <h4 class="font-semibold mb-3">⚙️ Settings</h4>
388
+ <div class="space-y-2 text-sm">
389
+ ${Object.entries(user.settings).map(([key, value]) =>
390
+ `<div><strong>${key}:</strong> ${value}</div>`
391
+ ).join('')}
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="mt-6">
397
+ <h4 class="font-semibold mb-3">📊 Recent Activity</h4>
398
+ <div class="max-h-48 overflow-y-auto">
399
+ ${user.recent_sessions.map(session => `
400
+ <div class="flex justify-between py-2 border-b text-sm">
401
+ <span>${session.activity}</span>
402
+ <span class="text-gray-500">${formatDate(session.timestamp)}</span>
403
+ </div>
404
+ `).join('')}
405
+ </div>
406
+ </div>
407
+
408
+ <div class="mt-6">
409
+ <h4 class="font-semibold mb-3">🎯 Token Usage</h4>
410
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
411
+ ${user.token_usage.map(usage => `
412
+ <div class="bg-gray-50 p-3 rounded">
413
+ <div class="font-medium">${usage.provider}</div>
414
+ <div class="text-sm text-gray-600">
415
+ Input: ${usage.input_tokens.toLocaleString()}<br>
416
+ Output: ${usage.output_tokens.toLocaleString()}<br>
417
+ Calls: ${usage.calls}
418
+ </div>
419
+ </div>
420
+ `).join('')}
421
+ </div>
422
+ </div>
423
+ `;
424
+
425
+ modal.classList.remove('hidden');
426
+ }
427
+
428
+ function closeUserModal() {
429
+ document.getElementById('userDetailModal').classList.add('hidden');
430
+ }
431
+
432
+ async function deleteUser(userId, email) {
433
+ if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
434
+ return;
435
+ }
436
+
437
+ try {
438
+ const response = await fetch(`/admin/users/${userId}`, {
439
+ method: 'DELETE'
440
+ });
441
+
442
+ const data = await response.json();
443
+
444
+ if (data.success) {
445
+ showToast('User deleted successfully', 'success');
446
+ loadUsers(); // Refresh the table
447
+ } else {
448
+ showToast('Failed to delete user', 'error');
449
+ }
450
+ } catch (error) {
451
+ console.error('Delete user error:', error);
452
+ showToast('Failed to delete user', 'error');
453
+ }
454
+ }
455
+
456
+ async function loadDatabaseSchema() {
457
+ try {
458
+ const response = await fetch('/admin/database/schema');
459
+ const data = await response.json();
460
+
461
+ if (data.success) {
462
+ updateDatabaseSchema(data.schema);
463
+ }
464
+ } catch (error) {
465
+ console.error('Schema loading error:', error);
466
+ showToast('Failed to load database schema', 'error');
467
+ }
468
+ }
469
+
470
+ function updateDatabaseSchema(schema) {
471
+ const container = document.getElementById('databaseSchema');
472
+
473
+ container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
474
+ <div class="border rounded-lg p-4">
475
+ <div class="flex justify-between items-center mb-3">
476
+ <h5 class="font-semibold text-lg">${tableName}</h5>
477
+ <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
478
+ ${tableInfo.row_count} rows
479
+ </span>
480
+ </div>
481
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
482
+ ${tableInfo.columns.map(col => `
483
+ <div class="flex items-center space-x-2">
484
+ <span class="font-medium">${col.name}</span>
485
+ <span class="text-gray-500">${col.type}</span>
486
+ ${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
487
+ ${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
488
+ </div>
489
+ `).join('')}
490
+ </div>
491
+ </div>
492
+ `).join('');
493
+ }
494
+
495
+ async function loadTokenUsage() {
496
+ try {
497
+ // For now, create sample charts with existing data
498
+ await createTokenUsageCharts();
499
+ } catch (error) {
500
+ console.error('Token usage loading error:', error);
501
+ showToast('Failed to load token usage data', 'error');
502
+ }
503
+ }
504
+
505
+ async function createTokenUsageCharts() {
506
+ // Provider Usage Chart
507
+ const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
508
+
509
+ new Chart(providerCtx, {
510
+ type: 'bar',
511
+ data: {
512
+ labels: ['Groq', 'Gemini'],
513
+ datasets: [{
514
+ label: 'Input Tokens',
515
+ data: [150000, 85000],
516
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
517
+ borderColor: 'rgba(59, 130, 246, 1)',
518
+ borderWidth: 1
519
+ }, {
520
+ label: 'Output Tokens',
521
+ data: [45000, 28000],
522
+ backgroundColor: 'rgba(147, 51, 234, 0.8)',
523
+ borderColor: 'rgba(147, 51, 234, 1)',
524
+ borderWidth: 1
525
+ }]
526
+ },
527
+ options: {
528
+ responsive: true,
529
+ plugins: {
530
+ title: {
531
+ display: true,
532
+ text: 'Token Usage by Provider'
533
+ }
534
+ },
535
+ scales: {
536
+ y: {
537
+ beginAtZero: true,
538
+ title: {
539
+ display: true,
540
+ text: 'Tokens'
541
+ }
542
+ }
543
+ }
544
+ }
545
+ });
546
+
547
+ // Cost Breakdown Chart
548
+ const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
549
+
550
+ new Chart(costCtx, {
551
+ type: 'pie',
552
+ data: {
553
+ labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
554
+ datasets: [{
555
+ data: [40, 25, 20, 15],
556
+ backgroundColor: [
557
+ 'rgba(34, 197, 94, 0.8)',
558
+ 'rgba(59, 130, 246, 0.8)',
559
+ 'rgba(147, 51, 234, 0.8)',
560
+ 'rgba(251, 146, 60, 0.8)'
561
+ ],
562
+ borderColor: [
563
+ 'rgba(34, 197, 94, 1)',
564
+ 'rgba(59, 130, 246, 1)',
565
+ 'rgba(147, 51, 234, 1)',
566
+ 'rgba(251, 146, 60, 1)'
567
+ ],
568
+ borderWidth: 2
569
+ }]
570
+ },
571
+ options: {
572
+ responsive: true,
573
+ plugins: {
574
+ title: {
575
+ display: true,
576
+ text: 'Cost Distribution by Operation Type'
577
+ },
578
+ legend: {
579
+ position: 'bottom'
580
+ }
581
+ }
582
+ }
583
+ });
584
+ }
585
+
586
+ function refreshUsers() {
587
+ currentPage = 1;
588
+ loadUsers();
589
+ }
590
+
591
+ function previousPage() {
592
+ if (currentPage > 1) {
593
+ currentPage--;
594
+ loadUsers();
595
+ }
596
+ }
597
+
598
+ function nextPage() {
599
+ if (usersData && currentPage < usersData.total_pages) {
600
+ currentPage++;
601
+ loadUsers();
602
+ }
603
+ }
604
+
605
+ function createCharts(stats) {
606
+ createUserGrowthChart(stats);
607
+ createTokenCostChart(stats);
608
+ }
609
+
610
+ function createUserGrowthChart(stats) {
611
+ const ctx = document.getElementById('userGrowthChart').getContext('2d');
612
+
613
+ // Generate sample data for last 30 days
614
+ const dates = [];
615
+ const userData = [];
616
+ const today = new Date();
617
+
618
+ for (let i = 29; i >= 0; i--) {
619
+ const date = new Date(today);
620
+ date.setDate(date.getDate() - i);
621
+ dates.push(date.toLocaleDateString());
622
+
623
+ // Simulate growth data (would come from real analytics)
624
+ userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
625
+ }
626
+
627
+ new Chart(ctx, {
628
+ type: 'line',
629
+ data: {
630
+ labels: dates,
631
+ datasets: [{
632
+ label: 'Total Users',
633
+ data: userData,
634
+ borderColor: 'rgb(59, 130, 246)',
635
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
636
+ fill: true,
637
+ tension: 0.4
638
+ }]
639
+ },
640
+ options: {
641
+ responsive: true,
642
+ plugins: {
643
+ title: {
644
+ display: true,
645
+ text: 'User Growth Over Time'
646
+ }
647
+ },
648
+ interaction: {
649
+ intersect: false,
650
+ },
651
+ scales: {
652
+ x: {
653
+ display: true,
654
+ title: {
655
+ display: true,
656
+ text: 'Date'
657
+ }
658
+ },
659
+ y: {
660
+ display: true,
661
+ title: {
662
+ display: true,
663
+ text: 'Users'
664
+ }
665
+ }
666
+ }
667
+ }
668
+ });
669
+ }
670
+
671
+ function createTokenCostChart(stats) {
672
+ const ctx = document.getElementById('tokenCostChart').getContext('2d');
673
+
674
+ // Sample cost data
675
+ const providers = ['Groq', 'Gemini'];
676
+ const costs = [
677
+ (stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
678
+ (stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
679
+ ];
680
+
681
+ new Chart(ctx, {
682
+ type: 'doughnut',
683
+ data: {
684
+ labels: providers,
685
+ datasets: [{
686
+ label: 'Cost ($)',
687
+ data: costs,
688
+ backgroundColor: [
689
+ 'rgba(59, 130, 246, 0.8)',
690
+ 'rgba(147, 51, 234, 0.8)'
691
+ ],
692
+ borderColor: [
693
+ 'rgba(59, 130, 246, 1)',
694
+ 'rgba(147, 51, 234, 1)'
695
+ ],
696
+ borderWidth: 2
697
+ }]
698
+ },
699
+ options: {
700
+ responsive: true,
701
+ plugins: {
702
+ title: {
703
+ display: true,
704
+ text: 'API Costs by Provider'
705
+ },
706
+ legend: {
707
+ position: 'bottom',
708
+ }
709
+ }
710
+ }
711
+ });
712
+ }
713
+
714
+ function exportUsers() {
715
+ window.open('/admin/export/users', '_blank');
716
+ showToast('Users data export started', 'success');
717
+ }
718
+
719
+ function exportTokens() {
720
+ window.open('/admin/export/tokens', '_blank');
721
+ showToast('Token usage data export started', 'success');
722
+ }
723
+
724
+ async function loadSystemHealth() {
725
+ try {
726
+ // Load system health metrics
727
+ const healthResponse = await fetch('/admin/system/health');
728
+ const healthData = await healthResponse.json();
729
+
730
+ if (healthData.success) {
731
+ updateSystemMetrics(healthData.health);
732
+ }
733
+
734
+ // Load system alerts
735
+ const alertsResponse = await fetch('/admin/system/alerts');
736
+ const alertsData = await alertsResponse.json();
737
+
738
+ if (alertsData.success) {
739
+ updateSystemAlerts(alertsData.alerts);
740
+ }
741
+ } catch (error) {
742
+ console.error('System health loading error:', error);
743
+ showToast('Failed to load system health data', 'error');
744
+ }
745
+ }
746
+
747
+ function updateSystemMetrics(health) {
748
+ // Memory metrics
749
+ if (health.memory) {
750
+ const memoryPercent = health.memory.percent || 0;
751
+ const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
752
+ const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
753
+
754
+ document.getElementById('memoryMetrics').innerHTML = `
755
+ <div class="space-y-2">
756
+ <div class="flex justify-between">
757
+ <span>Used:</span>
758
+ <span>${memoryUsed}GB / ${memoryTotal}GB</span>
759
+ </div>
760
+ <div class="w-full bg-gray-200 rounded-full h-2">
761
+ <div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
762
+ </div>
763
+ <div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
764
+ </div>
765
+ `;
766
+ }
767
+
768
+ // Disk metrics
769
+ if (health.disk) {
770
+ const diskPercent = health.disk.percent || 0;
771
+ const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
772
+ const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
773
+
774
+ document.getElementById('diskMetrics').innerHTML = `
775
+ <div class="space-y-2">
776
+ <div class="flex justify-between">
777
+ <span>Used:</span>
778
+ <span>${diskUsed}GB / ${diskTotal}GB</span>
779
+ </div>
780
+ <div class="w-full bg-gray-200 rounded-full h-2">
781
+ <div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
782
+ </div>
783
+ <div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
784
+ </div>
785
+ `;
786
+ }
787
+
788
+ // Database metrics
789
+ if (health.database) {
790
+ document.getElementById('databaseMetrics').innerHTML = `
791
+ <div class="space-y-2">
792
+ <div class="flex justify-between">
793
+ <span>Size:</span>
794
+ <span>${health.database.size_mb}MB</span>
795
+ </div>
796
+ <div class="flex justify-between">
797
+ <span>Uptime:</span>
798
+ <span>${health.uptime || 'Unknown'}</span>
799
+ </div>
800
+ </div>
801
+ `;
802
+ }
803
+
804
+ // Error logs
805
+ if (health.recent_errors && health.recent_errors.length > 0) {
806
+ document.getElementById('errorLogs').innerHTML = `
807
+ <div class="space-y-2 max-h-64 overflow-y-auto">
808
+ ${health.recent_errors.map(error => `
809
+ <div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
810
+ <div class="flex justify-between items-start">
811
+ <span class="font-medium text-red-800">${error.level}</span>
812
+ <span class="text-red-600 text-xs">${error.timestamp}</span>
813
+ </div>
814
+ <div class="text-red-700 mt-1">${error.message}</div>
815
+ <div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
816
+ </div>
817
+ `).join('')}
818
+ </div>
819
+ `;
820
+ } else {
821
+ document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
822
+ }
823
+ }
824
+
825
+ function updateSystemAlerts(alerts) {
826
+ const container = document.getElementById('systemAlerts');
827
+
828
+ if (alerts.length === 0) {
829
+ container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
830
+ return;
831
+ }
832
+
833
+ container.innerHTML = alerts.map(alert => {
834
+ const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
835
+ alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
836
+ 'bg-blue-50 border-blue-200';
837
+ const textColor = alert.type === 'error' ? 'text-red-800' :
838
+ alert.type === 'warning' ? 'text-yellow-800' :
839
+ 'text-blue-800';
840
+ const icon = alert.type === 'error' ? '🚨' :
841
+ alert.type === 'warning' ? '⚠️' : 'ℹ️';
842
+
843
+ return `
844
+ <div class="p-3 ${bgColor} border rounded-lg">
845
+ <div class="flex items-start gap-3">
846
+ <span class="text-lg">${icon}</span>
847
+ <div class="flex-1">
848
+ <div class="font-medium ${textColor}">${alert.message}</div>
849
+ <div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
850
+ </div>
851
+ </div>
852
+ </div>
853
+ `;
854
+ }).join('');
855
+ }
856
+
857
+ function formatDate(dateString) {
858
+ if (!dateString) return 'Never';
859
+ return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
860
+ }
861
+
862
+ function showToast(message, type = 'info') {
863
+ const toast = document.createElement('div');
864
+ toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
865
+ type === 'success' ? 'bg-green-500' :
866
+ type === 'error' ? 'bg-red-500' :
867
+ type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
868
+ } text-white`;
869
+
870
+ toast.textContent = message;
871
+ document.body.appendChild(toast);
872
+
873
+ setTimeout(() => {
874
+ toast.remove();
875
+ }, 5000);
876
+ }
877
+
878
+ // Add CSS for tab styling
879
+ const style = document.createElement('style');
880
+ style.textContent = `
881
+ .admin-tab {
882
+ padding: 0.5rem 1rem;
883
+ border-bottom: 2px solid transparent;
884
+ font-medium: 500;
885
+ text-decoration: none;
886
+ transition: all 0.2s;
887
+ }
888
+ .admin-tab:hover {
889
+ text-decoration: none;
890
+ border-bottom-color: #d1d5db;
891
+ }
892
+ .admin-tab.active {
893
+ border-bottom-color: #3b82f6;
894
+ color: #3b82f6;
895
+ }
896
+ `;
897
+ document.head.appendChild(style);
898
+ </script>
899
+ </body>
900
  </html>
templates/base.html CHANGED
@@ -1,900 +1,900 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>English Helper - Admin Panel</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
- <style>
10
- /* Tailwind utility classes should be used directly in HTML, not via @apply in <style> */
11
- .spinner {
12
- border: 3px solid #f3f3f3;
13
- border-top: 3px solid #3498db;
14
- border-radius: 50%;
15
- width: 20px;
16
- height: 20px;
17
- animation: spin 1s linear infinite;
18
- display: inline-block;
19
- margin-right: 10px;
20
- }
21
- @keyframes spin {
22
- 0% { transform: rotate(0deg); }
23
- 100% { transform: rotate(360deg); }
24
- }
25
- .table-container {
26
- max-height: 600px;
27
- overflow-y: auto;
28
- }
29
- </style>
30
- </head>
31
- <body class="bg-gray-100 min-h-screen">
32
- <!-- Admin Login Modal removed: admin panel is always accessible -->
33
-
34
- <!-- Admin Interface -->
35
- <div id="adminInterface">
36
- <!-- Header -->
37
- <header class="bg-white shadow-md">
38
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
39
- <div class="flex justify-between items-center py-4">
40
- <div class="flex items-center">
41
- <h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
42
- </div>
43
- <!-- Admin user info and logout removed -->
44
- </div>
45
- </div>
46
- </header>
47
-
48
- <!-- Navigation Tabs -->
49
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
50
- <div class="border-b border-gray-200">
51
- <nav class="-mb-px flex space-x-8">
52
- <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
53
- 📊 Dashboard
54
- </button>
55
- <button onclick="showTab('users')" class="admin-tab" data-tab="users">
56
- 👥 Users
57
- </button>
58
- <button onclick="showTab('database')" class="admin-tab" data-tab="database">
59
- 🗄️ Database
60
- </button>
61
- <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
62
- 🎯 Token Usage
63
- </button>
64
- <button onclick="showTab('system')" class="admin-tab" data-tab="system">
65
- 🖥️ System Health
66
- </button>
67
- </nav>
68
- </div>
69
- </div>
70
-
71
- <!-- Tab Content -->
72
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
73
- <!-- Dashboard Tab -->
74
- <div id="dashboardTab" class="tab-content">
75
- <h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
76
-
77
- <!-- System Stats Cards -->
78
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
79
- <div class="stat-card">
80
- <h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
81
- <p id="totalUsers" class="text-3xl font-bold">-</p>
82
- <p class="text-sm opacity-80">
83
- <span id="newUsersWeek">-</span> this week
84
- </p>
85
- </div>
86
- <div class="stat-card">
87
- <h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
88
- <p id="totalSessions" class="text-3xl font-bold">-</p>
89
- <p class="text-sm opacity-80">All time</p>
90
- </div>
91
- <div class="stat-card">
92
- <h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
93
- <p id="totalTokens" class="text-3xl font-bold">-</p>
94
- <p class="text-sm opacity-80">
95
- $<span id="estimatedCost">-</span> estimated cost
96
- </p>
97
- </div>
98
- <div class="stat-card">
99
- <h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
100
- <p id="totalContent" class="text-3xl font-bold">-</p>
101
- <p class="text-sm opacity-80">Articles & flashcards</p>
102
- </div>
103
- </div>
104
-
105
- <!-- Charts Section -->
106
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
107
- <div class="admin-card">
108
- <h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
109
- <canvas id="userGrowthChart" width="400" height="200"></canvas>
110
- </div>
111
- <div class="admin-card">
112
- <h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
113
- <canvas id="tokenCostChart" width="400" height="200"></canvas>
114
- </div>
115
- </div>
116
-
117
- <!-- Recent Activity -->
118
- <div class="admin-card">
119
- <h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
120
- <div id="recentActivity" class="space-y-2">
121
- <div class="text-center text-gray-500 py-4">Loading recent activity...</div>
122
- </div>
123
- </div>
124
- </div>
125
-
126
- <!-- Users Tab -->
127
- <!-- Users Tab removed: user management is not available in open version -->
128
-
129
- <!-- Database Tab -->
130
- <div id="databaseTab" class="tab-content hidden">
131
- <h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
132
-
133
- <div class="admin-card">
134
- <h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
135
- <div id="databaseSchema" class="space-y-4">
136
- <div class="text-center text-gray-500 py-4">Loading schema...</div>
137
- </div>
138
- </div>
139
- </div>
140
-
141
- <!-- Token Usage Tab -->
142
- <div id="tokensTab" class="tab-content hidden">
143
- <div class="flex justify-between items-center mb-6">
144
- <h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
145
- <button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
146
- 📥 Export Token Data
147
- </button>
148
- </div>
149
-
150
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
151
- <div class="admin-card">
152
- <h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
153
- <canvas id="providerUsageChart" width="400" height="300"></canvas>
154
- </div>
155
- <div class="admin-card">
156
- <h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
157
- <canvas id="costBreakdownChart" width="400" height="300"></canvas>
158
- </div>
159
- </div>
160
- </div>
161
-
162
- <!-- System Health Tab -->
163
- <div id="systemTab" class="tab-content hidden">
164
- <h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
165
-
166
- <!-- System Alerts -->
167
- <div class="admin-card mb-6">
168
- <h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
169
- <div id="systemAlerts">
170
- <div class="text-center text-gray-500 py-4">Loading alerts...</div>
171
- </div>
172
- </div>
173
-
174
- <!-- System Metrics -->
175
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
176
- <div class="admin-card">
177
- <h4 class="font-semibold mb-3">💾 Memory Usage</h4>
178
- <div id="memoryMetrics">
179
- <div class="text-sm text-gray-500">Loading...</div>
180
- </div>
181
- </div>
182
-
183
- <div class="admin-card">
184
- <h4 class="font-semibold mb-3">💿 Disk Usage</h4>
185
- <div id="diskMetrics">
186
- <div class="text-sm text-gray-500">Loading...</div>
187
- </div>
188
- </div>
189
-
190
- <div class="admin-card">
191
- <h4 class="font-semibold mb-3">🗄️ Database</h4>
192
- <div id="databaseMetrics">
193
- <div class="text-sm text-gray-500">Loading...</div>
194
- </div>
195
- </div>
196
- </div>
197
-
198
- <!-- Error Logs -->
199
- <div class="admin-card">
200
- <h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
201
- <div id="errorLogs">
202
- <div class="text-center text-gray-500 py-4">Loading error logs...</div>
203
- </div>
204
- </div>
205
- </div>
206
- </div>
207
- </div>
208
-
209
- <!-- User Detail Modal removed -->
210
-
211
- <!-- Toast Notifications -->
212
- <div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
213
-
214
- <script>
215
- // User logic, authentication, and user management removed. Admin panel is always accessible.
216
-
217
- // Tab management (users tab removed)
218
- function showTab(tabName) {
219
- document.querySelectorAll('.tab-content').forEach(tab => {
220
- tab.classList.add('hidden');
221
- });
222
- document.querySelectorAll('.admin-tab').forEach(btn => {
223
- btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
224
- btn.classList.add('border-transparent', 'text-gray-500');
225
- });
226
- document.getElementById(tabName + 'Tab').classList.remove('hidden');
227
- const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
228
- activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
229
- activeBtn.classList.remove('border-transparent', 'text-gray-500');
230
- if (tabName === 'dashboard') {
231
- loadDashboard();
232
- } else if (tabName === 'database') {
233
- loadDatabaseSchema();
234
- } else if (tabName === 'tokens') {
235
- loadTokenUsage();
236
- } else if (tabName === 'system') {
237
- loadSystemHealth();
238
- }
239
- }
240
-
241
- async function loadDashboard() {
242
- try {
243
- const response = await fetch('/admin/dashboard');
244
- const data = await response.json();
245
-
246
- if (data.success) {
247
- const stats = data.stats;
248
-
249
- // Update stat cards
250
- document.getElementById('totalUsers').textContent = stats.users?.total || 0;
251
- document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
252
- document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
253
- document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
254
- document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
255
- document.getElementById('totalContent').textContent =
256
- (stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
257
-
258
- // Update recent activity
259
- updateRecentActivity(stats.recent_activity || []);
260
-
261
- // Create charts
262
- createCharts(stats);
263
- }
264
- } catch (error) {
265
- console.error('Dashboard loading error:', error);
266
- showToast('Failed to load dashboard', 'error');
267
- }
268
- }
269
-
270
- function updateRecentActivity(activities) {
271
- const container = document.getElementById('recentActivity');
272
-
273
- if (activities.length === 0) {
274
- container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
275
- return;
276
- }
277
-
278
- container.innerHTML = activities.map(activity => `
279
- <div class="flex items-center justify-between py-2 border-b border-gray-100">
280
- <div>
281
- <span class="font-medium">${activity.user}</span>
282
- <span class="text-gray-600">performed ${activity.activity}</span>
283
- </div>
284
- <span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
285
- </div>
286
- `).join('');
287
- }
288
-
289
- async function loadUsers() {
290
- try {
291
- const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
292
- const data = await response.json();
293
-
294
- if (data.success) {
295
- usersData = data.data;
296
- updateUsersTable(usersData.users);
297
- updateUsersPagination(usersData);
298
- }
299
- } catch (error) {
300
- console.error('Users loading error:', error);
301
- showToast('Failed to load users', 'error');
302
- }
303
- }
304
-
305
- function updateUsersTable(users) {
306
- const tbody = document.getElementById('usersTableBody');
307
-
308
- if (users.length === 0) {
309
- tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
310
- return;
311
- }
312
-
313
- tbody.innerHTML = users.map(user => `
314
- <tr class="border-b hover:bg-gray-50">
315
- <td class="px-4 py-3">
316
- <div>
317
- <div class="font-medium">${user.email}</div>
318
- <div class="text-sm text-gray-500">ID: ${user.id}</div>
319
- </div>
320
- </td>
321
- <td class="px-4 py-3 text-sm text-gray-600">
322
- ${formatDate(user.created_at)}
323
- </td>
324
- <td class="px-4 py-3">
325
- <div class="text-sm">
326
- <div>${user.session_count} sessions</div>
327
- <div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
328
- </div>
329
- </td>
330
- <td class="px-4 py-3">
331
- <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'}">
332
- ${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
333
- </span>
334
- </td>
335
- <td class="px-4 py-3">
336
- <div class="flex space-x-2">
337
- <button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
338
- <button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
339
- </div>
340
- </td>
341
- </tr>
342
- `).join('');
343
- }
344
-
345
- function updateUsersPagination(data) {
346
- document.getElementById('usersInfo').textContent =
347
- `Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
348
-
349
- document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
350
-
351
- document.getElementById('prevPageBtn').disabled = data.page <= 1;
352
- document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
353
- }
354
-
355
- async function viewUser(userId) {
356
- try {
357
- const response = await fetch(`/admin/users/${userId}`);
358
- const data = await response.json();
359
-
360
- if (data.success) {
361
- showUserModal(data.user);
362
- }
363
- } catch (error) {
364
- console.error('User details error:', error);
365
- showToast('Failed to load user details', 'error');
366
- }
367
- }
368
-
369
- function showUserModal(user) {
370
- const modal = document.getElementById('userDetailModal');
371
- const content = document.getElementById('userDetailContent');
372
-
373
- content.innerHTML = `
374
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
- <div>
376
- <h4 class="font-semibold mb-3">📋 Basic Information</h4>
377
- <div class="space-y-2 text-sm">
378
- <div><strong>Email:</strong> ${user.user.email}</div>
379
- <div><strong>ID:</strong> ${user.user.id}</div>
380
- <div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
381
- <div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
382
- <div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
383
- </div>
384
- </div>
385
-
386
- <div>
387
- <h4 class="font-semibold mb-3">⚙️ Settings</h4>
388
- <div class="space-y-2 text-sm">
389
- ${Object.entries(user.settings).map(([key, value]) =>
390
- `<div><strong>${key}:</strong> ${value}</div>`
391
- ).join('')}
392
- </div>
393
- </div>
394
- </div>
395
-
396
- <div class="mt-6">
397
- <h4 class="font-semibold mb-3">📊 Recent Activity</h4>
398
- <div class="max-h-48 overflow-y-auto">
399
- ${user.recent_sessions.map(session => `
400
- <div class="flex justify-between py-2 border-b text-sm">
401
- <span>${session.activity}</span>
402
- <span class="text-gray-500">${formatDate(session.timestamp)}</span>
403
- </div>
404
- `).join('')}
405
- </div>
406
- </div>
407
-
408
- <div class="mt-6">
409
- <h4 class="font-semibold mb-3">🎯 Token Usage</h4>
410
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
411
- ${user.token_usage.map(usage => `
412
- <div class="bg-gray-50 p-3 rounded">
413
- <div class="font-medium">${usage.provider}</div>
414
- <div class="text-sm text-gray-600">
415
- Input: ${usage.input_tokens.toLocaleString()}<br>
416
- Output: ${usage.output_tokens.toLocaleString()}<br>
417
- Calls: ${usage.calls}
418
- </div>
419
- </div>
420
- `).join('')}
421
- </div>
422
- </div>
423
- `;
424
-
425
- modal.classList.remove('hidden');
426
- }
427
-
428
- function closeUserModal() {
429
- document.getElementById('userDetailModal').classList.add('hidden');
430
- }
431
-
432
- async function deleteUser(userId, email) {
433
- if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
434
- return;
435
- }
436
-
437
- try {
438
- const response = await fetch(`/admin/users/${userId}`, {
439
- method: 'DELETE'
440
- });
441
-
442
- const data = await response.json();
443
-
444
- if (data.success) {
445
- showToast('User deleted successfully', 'success');
446
- loadUsers(); // Refresh the table
447
- } else {
448
- showToast('Failed to delete user', 'error');
449
- }
450
- } catch (error) {
451
- console.error('Delete user error:', error);
452
- showToast('Failed to delete user', 'error');
453
- }
454
- }
455
-
456
- async function loadDatabaseSchema() {
457
- try {
458
- const response = await fetch('/admin/database/schema');
459
- const data = await response.json();
460
-
461
- if (data.success) {
462
- updateDatabaseSchema(data.schema);
463
- }
464
- } catch (error) {
465
- console.error('Schema loading error:', error);
466
- showToast('Failed to load database schema', 'error');
467
- }
468
- }
469
-
470
- function updateDatabaseSchema(schema) {
471
- const container = document.getElementById('databaseSchema');
472
-
473
- container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
474
- <div class="border rounded-lg p-4">
475
- <div class="flex justify-between items-center mb-3">
476
- <h5 class="font-semibold text-lg">${tableName}</h5>
477
- <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
478
- ${tableInfo.row_count} rows
479
- </span>
480
- </div>
481
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
482
- ${tableInfo.columns.map(col => `
483
- <div class="flex items-center space-x-2">
484
- <span class="font-medium">${col.name}</span>
485
- <span class="text-gray-500">${col.type}</span>
486
- ${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
487
- ${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
488
- </div>
489
- `).join('')}
490
- </div>
491
- </div>
492
- `).join('');
493
- }
494
-
495
- async function loadTokenUsage() {
496
- try {
497
- // For now, create sample charts with existing data
498
- await createTokenUsageCharts();
499
- } catch (error) {
500
- console.error('Token usage loading error:', error);
501
- showToast('Failed to load token usage data', 'error');
502
- }
503
- }
504
-
505
- async function createTokenUsageCharts() {
506
- // Provider Usage Chart
507
- const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
508
-
509
- new Chart(providerCtx, {
510
- type: 'bar',
511
- data: {
512
- labels: ['Groq', 'Gemini'],
513
- datasets: [{
514
- label: 'Input Tokens',
515
- data: [150000, 85000],
516
- backgroundColor: 'rgba(59, 130, 246, 0.8)',
517
- borderColor: 'rgba(59, 130, 246, 1)',
518
- borderWidth: 1
519
- }, {
520
- label: 'Output Tokens',
521
- data: [45000, 28000],
522
- backgroundColor: 'rgba(147, 51, 234, 0.8)',
523
- borderColor: 'rgba(147, 51, 234, 1)',
524
- borderWidth: 1
525
- }]
526
- },
527
- options: {
528
- responsive: true,
529
- plugins: {
530
- title: {
531
- display: true,
532
- text: 'Token Usage by Provider'
533
- }
534
- },
535
- scales: {
536
- y: {
537
- beginAtZero: true,
538
- title: {
539
- display: true,
540
- text: 'Tokens'
541
- }
542
- }
543
- }
544
- }
545
- });
546
-
547
- // Cost Breakdown Chart
548
- const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
549
-
550
- new Chart(costCtx, {
551
- type: 'pie',
552
- data: {
553
- labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
554
- datasets: [{
555
- data: [40, 25, 20, 15],
556
- backgroundColor: [
557
- 'rgba(34, 197, 94, 0.8)',
558
- 'rgba(59, 130, 246, 0.8)',
559
- 'rgba(147, 51, 234, 0.8)',
560
- 'rgba(251, 146, 60, 0.8)'
561
- ],
562
- borderColor: [
563
- 'rgba(34, 197, 94, 1)',
564
- 'rgba(59, 130, 246, 1)',
565
- 'rgba(147, 51, 234, 1)',
566
- 'rgba(251, 146, 60, 1)'
567
- ],
568
- borderWidth: 2
569
- }]
570
- },
571
- options: {
572
- responsive: true,
573
- plugins: {
574
- title: {
575
- display: true,
576
- text: 'Cost Distribution by Operation Type'
577
- },
578
- legend: {
579
- position: 'bottom'
580
- }
581
- }
582
- }
583
- });
584
- }
585
-
586
- function refreshUsers() {
587
- currentPage = 1;
588
- loadUsers();
589
- }
590
-
591
- function previousPage() {
592
- if (currentPage > 1) {
593
- currentPage--;
594
- loadUsers();
595
- }
596
- }
597
-
598
- function nextPage() {
599
- if (usersData && currentPage < usersData.total_pages) {
600
- currentPage++;
601
- loadUsers();
602
- }
603
- }
604
-
605
- function createCharts(stats) {
606
- createUserGrowthChart(stats);
607
- createTokenCostChart(stats);
608
- }
609
-
610
- function createUserGrowthChart(stats) {
611
- const ctx = document.getElementById('userGrowthChart').getContext('2d');
612
-
613
- // Generate sample data for last 30 days
614
- const dates = [];
615
- const userData = [];
616
- const today = new Date();
617
-
618
- for (let i = 29; i >= 0; i--) {
619
- const date = new Date(today);
620
- date.setDate(date.getDate() - i);
621
- dates.push(date.toLocaleDateString());
622
-
623
- // Simulate growth data (would come from real analytics)
624
- userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
625
- }
626
-
627
- new Chart(ctx, {
628
- type: 'line',
629
- data: {
630
- labels: dates,
631
- datasets: [{
632
- label: 'Total Users',
633
- data: userData,
634
- borderColor: 'rgb(59, 130, 246)',
635
- backgroundColor: 'rgba(59, 130, 246, 0.1)',
636
- fill: true,
637
- tension: 0.4
638
- }]
639
- },
640
- options: {
641
- responsive: true,
642
- plugins: {
643
- title: {
644
- display: true,
645
- text: 'User Growth Over Time'
646
- }
647
- },
648
- interaction: {
649
- intersect: false,
650
- },
651
- scales: {
652
- x: {
653
- display: true,
654
- title: {
655
- display: true,
656
- text: 'Date'
657
- }
658
- },
659
- y: {
660
- display: true,
661
- title: {
662
- display: true,
663
- text: 'Users'
664
- }
665
- }
666
- }
667
- }
668
- });
669
- }
670
-
671
- function createTokenCostChart(stats) {
672
- const ctx = document.getElementById('tokenCostChart').getContext('2d');
673
-
674
- // Sample cost data
675
- const providers = ['Groq', 'Gemini'];
676
- const costs = [
677
- (stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
678
- (stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
679
- ];
680
-
681
- new Chart(ctx, {
682
- type: 'doughnut',
683
- data: {
684
- labels: providers,
685
- datasets: [{
686
- label: 'Cost ($)',
687
- data: costs,
688
- backgroundColor: [
689
- 'rgba(59, 130, 246, 0.8)',
690
- 'rgba(147, 51, 234, 0.8)'
691
- ],
692
- borderColor: [
693
- 'rgba(59, 130, 246, 1)',
694
- 'rgba(147, 51, 234, 1)'
695
- ],
696
- borderWidth: 2
697
- }]
698
- },
699
- options: {
700
- responsive: true,
701
- plugins: {
702
- title: {
703
- display: true,
704
- text: 'API Costs by Provider'
705
- },
706
- legend: {
707
- position: 'bottom',
708
- }
709
- }
710
- }
711
- });
712
- }
713
-
714
- function exportUsers() {
715
- window.open('/admin/export/users', '_blank');
716
- showToast('Users data export started', 'success');
717
- }
718
-
719
- function exportTokens() {
720
- window.open('/admin/export/tokens', '_blank');
721
- showToast('Token usage data export started', 'success');
722
- }
723
-
724
- async function loadSystemHealth() {
725
- try {
726
- // Load system health metrics
727
- const healthResponse = await fetch('/admin/system/health');
728
- const healthData = await healthResponse.json();
729
-
730
- if (healthData.success) {
731
- updateSystemMetrics(healthData.health);
732
- }
733
-
734
- // Load system alerts
735
- const alertsResponse = await fetch('/admin/system/alerts');
736
- const alertsData = await alertsResponse.json();
737
-
738
- if (alertsData.success) {
739
- updateSystemAlerts(alertsData.alerts);
740
- }
741
- } catch (error) {
742
- console.error('System health loading error:', error);
743
- showToast('Failed to load system health data', 'error');
744
- }
745
- }
746
-
747
- function updateSystemMetrics(health) {
748
- // Memory metrics
749
- if (health.memory) {
750
- const memoryPercent = health.memory.percent || 0;
751
- const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
752
- const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
753
-
754
- document.getElementById('memoryMetrics').innerHTML = `
755
- <div class="space-y-2">
756
- <div class="flex justify-between">
757
- <span>Used:</span>
758
- <span>${memoryUsed}GB / ${memoryTotal}GB</span>
759
- </div>
760
- <div class="w-full bg-gray-200 rounded-full h-2">
761
- <div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
762
- </div>
763
- <div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
764
- </div>
765
- `;
766
- }
767
-
768
- // Disk metrics
769
- if (health.disk) {
770
- const diskPercent = health.disk.percent || 0;
771
- const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
772
- const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
773
-
774
- document.getElementById('diskMetrics').innerHTML = `
775
- <div class="space-y-2">
776
- <div class="flex justify-between">
777
- <span>Used:</span>
778
- <span>${diskUsed}GB / ${diskTotal}GB</span>
779
- </div>
780
- <div class="w-full bg-gray-200 rounded-full h-2">
781
- <div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
782
- </div>
783
- <div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
784
- </div>
785
- `;
786
- }
787
-
788
- // Database metrics
789
- if (health.database) {
790
- document.getElementById('databaseMetrics').innerHTML = `
791
- <div class="space-y-2">
792
- <div class="flex justify-between">
793
- <span>Size:</span>
794
- <span>${health.database.size_mb}MB</span>
795
- </div>
796
- <div class="flex justify-between">
797
- <span>Uptime:</span>
798
- <span>${health.uptime || 'Unknown'}</span>
799
- </div>
800
- </div>
801
- `;
802
- }
803
-
804
- // Error logs
805
- if (health.recent_errors && health.recent_errors.length > 0) {
806
- document.getElementById('errorLogs').innerHTML = `
807
- <div class="space-y-2 max-h-64 overflow-y-auto">
808
- ${health.recent_errors.map(error => `
809
- <div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
810
- <div class="flex justify-between items-start">
811
- <span class="font-medium text-red-800">${error.level}</span>
812
- <span class="text-red-600 text-xs">${error.timestamp}</span>
813
- </div>
814
- <div class="text-red-700 mt-1">${error.message}</div>
815
- <div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
816
- </div>
817
- `).join('')}
818
- </div>
819
- `;
820
- } else {
821
- document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
822
- }
823
- }
824
-
825
- function updateSystemAlerts(alerts) {
826
- const container = document.getElementById('systemAlerts');
827
-
828
- if (alerts.length === 0) {
829
- container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
830
- return;
831
- }
832
-
833
- container.innerHTML = alerts.map(alert => {
834
- const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
835
- alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
836
- 'bg-blue-50 border-blue-200';
837
- const textColor = alert.type === 'error' ? 'text-red-800' :
838
- alert.type === 'warning' ? 'text-yellow-800' :
839
- 'text-blue-800';
840
- const icon = alert.type === 'error' ? '🚨' :
841
- alert.type === 'warning' ? '⚠️' : 'ℹ️';
842
-
843
- return `
844
- <div class="p-3 ${bgColor} border rounded-lg">
845
- <div class="flex items-start gap-3">
846
- <span class="text-lg">${icon}</span>
847
- <div class="flex-1">
848
- <div class="font-medium ${textColor}">${alert.message}</div>
849
- <div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
850
- </div>
851
- </div>
852
- </div>
853
- `;
854
- }).join('');
855
- }
856
-
857
- function formatDate(dateString) {
858
- if (!dateString) return 'Never';
859
- return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
860
- }
861
-
862
- function showToast(message, type = 'info') {
863
- const toast = document.createElement('div');
864
- toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
865
- type === 'success' ? 'bg-green-500' :
866
- type === 'error' ? 'bg-red-500' :
867
- type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
868
- } text-white`;
869
-
870
- toast.textContent = message;
871
- document.body.appendChild(toast);
872
-
873
- setTimeout(() => {
874
- toast.remove();
875
- }, 5000);
876
- }
877
-
878
- // Add CSS for tab styling
879
- const style = document.createElement('style');
880
- style.textContent = `
881
- .admin-tab {
882
- padding: 0.5rem 1rem;
883
- border-bottom: 2px solid transparent;
884
- font-medium: 500;
885
- text-decoration: none;
886
- transition: all 0.2s;
887
- }
888
- .admin-tab:hover {
889
- text-decoration: none;
890
- border-bottom-color: #d1d5db;
891
- }
892
- .admin-tab.active {
893
- border-bottom-color: #3b82f6;
894
- color: #3b82f6;
895
- }
896
- `;
897
- document.head.appendChild(style);
898
- </script>
899
- </body>
900
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>English Helper - Admin Panel</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ /* Tailwind utility classes should be used directly in HTML, not via @apply in <style> */
11
+ .spinner {
12
+ border: 3px solid #f3f3f3;
13
+ border-top: 3px solid #3498db;
14
+ border-radius: 50%;
15
+ width: 20px;
16
+ height: 20px;
17
+ animation: spin 1s linear infinite;
18
+ display: inline-block;
19
+ margin-right: 10px;
20
+ }
21
+ @keyframes spin {
22
+ 0% { transform: rotate(0deg); }
23
+ 100% { transform: rotate(360deg); }
24
+ }
25
+ .table-container {
26
+ max-height: 600px;
27
+ overflow-y: auto;
28
+ }
29
+ </style>
30
+ </head>
31
+ <body class="bg-gray-100 min-h-screen">
32
+ <!-- Admin Login Modal removed: admin panel is always accessible -->
33
+
34
+ <!-- Admin Interface -->
35
+ <div id="adminInterface">
36
+ <!-- Header -->
37
+ <header class="bg-white shadow-md">
38
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
39
+ <div class="flex justify-between items-center py-4">
40
+ <div class="flex items-center">
41
+ <h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
42
+ </div>
43
+ <!-- Admin user info and logout removed -->
44
+ </div>
45
+ </div>
46
+ </header>
47
+
48
+ <!-- Navigation Tabs -->
49
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
50
+ <div class="border-b border-gray-200">
51
+ <nav class="-mb-px flex space-x-8">
52
+ <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
53
+ 📊 Dashboard
54
+ </button>
55
+ <button onclick="showTab('users')" class="admin-tab" data-tab="users">
56
+ 👥 Users
57
+ </button>
58
+ <button onclick="showTab('database')" class="admin-tab" data-tab="database">
59
+ 🗄️ Database
60
+ </button>
61
+ <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
62
+ 🎯 Token Usage
63
+ </button>
64
+ <button onclick="showTab('system')" class="admin-tab" data-tab="system">
65
+ 🖥️ System Health
66
+ </button>
67
+ </nav>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Tab Content -->
72
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
73
+ <!-- Dashboard Tab -->
74
+ <div id="dashboardTab" class="tab-content">
75
+ <h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
76
+
77
+ <!-- System Stats Cards -->
78
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
79
+ <div class="stat-card">
80
+ <h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
81
+ <p id="totalUsers" class="text-3xl font-bold">-</p>
82
+ <p class="text-sm opacity-80">
83
+ <span id="newUsersWeek">-</span> this week
84
+ </p>
85
+ </div>
86
+ <div class="stat-card">
87
+ <h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
88
+ <p id="totalSessions" class="text-3xl font-bold">-</p>
89
+ <p class="text-sm opacity-80">All time</p>
90
+ </div>
91
+ <div class="stat-card">
92
+ <h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
93
+ <p id="totalTokens" class="text-3xl font-bold">-</p>
94
+ <p class="text-sm opacity-80">
95
+ $<span id="estimatedCost">-</span> estimated cost
96
+ </p>
97
+ </div>
98
+ <div class="stat-card">
99
+ <h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
100
+ <p id="totalContent" class="text-3xl font-bold">-</p>
101
+ <p class="text-sm opacity-80">Articles & flashcards</p>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Charts Section -->
106
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
107
+ <div class="admin-card">
108
+ <h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
109
+ <canvas id="userGrowthChart" width="400" height="200"></canvas>
110
+ </div>
111
+ <div class="admin-card">
112
+ <h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
113
+ <canvas id="tokenCostChart" width="400" height="200"></canvas>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Recent Activity -->
118
+ <div class="admin-card">
119
+ <h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
120
+ <div id="recentActivity" class="space-y-2">
121
+ <div class="text-center text-gray-500 py-4">Loading recent activity...</div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Users Tab -->
127
+ <!-- Users Tab removed: user management is not available in open version -->
128
+
129
+ <!-- Database Tab -->
130
+ <div id="databaseTab" class="tab-content hidden">
131
+ <h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
132
+
133
+ <div class="admin-card">
134
+ <h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
135
+ <div id="databaseSchema" class="space-y-4">
136
+ <div class="text-center text-gray-500 py-4">Loading schema...</div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Token Usage Tab -->
142
+ <div id="tokensTab" class="tab-content hidden">
143
+ <div class="flex justify-between items-center mb-6">
144
+ <h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
145
+ <button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
146
+ 📥 Export Token Data
147
+ </button>
148
+ </div>
149
+
150
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
151
+ <div class="admin-card">
152
+ <h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
153
+ <canvas id="providerUsageChart" width="400" height="300"></canvas>
154
+ </div>
155
+ <div class="admin-card">
156
+ <h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
157
+ <canvas id="costBreakdownChart" width="400" height="300"></canvas>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- System Health Tab -->
163
+ <div id="systemTab" class="tab-content hidden">
164
+ <h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
165
+
166
+ <!-- System Alerts -->
167
+ <div class="admin-card mb-6">
168
+ <h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
169
+ <div id="systemAlerts">
170
+ <div class="text-center text-gray-500 py-4">Loading alerts...</div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- System Metrics -->
175
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
176
+ <div class="admin-card">
177
+ <h4 class="font-semibold mb-3">💾 Memory Usage</h4>
178
+ <div id="memoryMetrics">
179
+ <div class="text-sm text-gray-500">Loading...</div>
180
+ </div>
181
+ </div>
182
+
183
+ <div class="admin-card">
184
+ <h4 class="font-semibold mb-3">💿 Disk Usage</h4>
185
+ <div id="diskMetrics">
186
+ <div class="text-sm text-gray-500">Loading...</div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="admin-card">
191
+ <h4 class="font-semibold mb-3">🗄️ Database</h4>
192
+ <div id="databaseMetrics">
193
+ <div class="text-sm text-gray-500">Loading...</div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Error Logs -->
199
+ <div class="admin-card">
200
+ <h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
201
+ <div id="errorLogs">
202
+ <div class="text-center text-gray-500 py-4">Loading error logs...</div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- User Detail Modal removed -->
210
+
211
+ <!-- Toast Notifications -->
212
+ <div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
213
+
214
+ <script>
215
+ // User logic, authentication, and user management removed. Admin panel is always accessible.
216
+
217
+ // Tab management (users tab removed)
218
+ function showTab(tabName) {
219
+ document.querySelectorAll('.tab-content').forEach(tab => {
220
+ tab.classList.add('hidden');
221
+ });
222
+ document.querySelectorAll('.admin-tab').forEach(btn => {
223
+ btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
224
+ btn.classList.add('border-transparent', 'text-gray-500');
225
+ });
226
+ document.getElementById(tabName + 'Tab').classList.remove('hidden');
227
+ const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
228
+ activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
229
+ activeBtn.classList.remove('border-transparent', 'text-gray-500');
230
+ if (tabName === 'dashboard') {
231
+ loadDashboard();
232
+ } else if (tabName === 'database') {
233
+ loadDatabaseSchema();
234
+ } else if (tabName === 'tokens') {
235
+ loadTokenUsage();
236
+ } else if (tabName === 'system') {
237
+ loadSystemHealth();
238
+ }
239
+ }
240
+
241
+ async function loadDashboard() {
242
+ try {
243
+ const response = await fetch('/admin/dashboard');
244
+ const data = await response.json();
245
+
246
+ if (data.success) {
247
+ const stats = data.stats;
248
+
249
+ // Update stat cards
250
+ document.getElementById('totalUsers').textContent = stats.users?.total || 0;
251
+ document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
252
+ document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
253
+ document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
254
+ document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
255
+ document.getElementById('totalContent').textContent =
256
+ (stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
257
+
258
+ // Update recent activity
259
+ updateRecentActivity(stats.recent_activity || []);
260
+
261
+ // Create charts
262
+ createCharts(stats);
263
+ }
264
+ } catch (error) {
265
+ console.error('Dashboard loading error:', error);
266
+ showToast('Failed to load dashboard', 'error');
267
+ }
268
+ }
269
+
270
+ function updateRecentActivity(activities) {
271
+ const container = document.getElementById('recentActivity');
272
+
273
+ if (activities.length === 0) {
274
+ container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
275
+ return;
276
+ }
277
+
278
+ container.innerHTML = activities.map(activity => `
279
+ <div class="flex items-center justify-between py-2 border-b border-gray-100">
280
+ <div>
281
+ <span class="font-medium">${activity.user}</span>
282
+ <span class="text-gray-600">performed ${activity.activity}</span>
283
+ </div>
284
+ <span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
285
+ </div>
286
+ `).join('');
287
+ }
288
+
289
+ async function loadUsers() {
290
+ try {
291
+ const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
292
+ const data = await response.json();
293
+
294
+ if (data.success) {
295
+ usersData = data.data;
296
+ updateUsersTable(usersData.users);
297
+ updateUsersPagination(usersData);
298
+ }
299
+ } catch (error) {
300
+ console.error('Users loading error:', error);
301
+ showToast('Failed to load users', 'error');
302
+ }
303
+ }
304
+
305
+ function updateUsersTable(users) {
306
+ const tbody = document.getElementById('usersTableBody');
307
+
308
+ if (users.length === 0) {
309
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
310
+ return;
311
+ }
312
+
313
+ tbody.innerHTML = users.map(user => `
314
+ <tr class="border-b hover:bg-gray-50">
315
+ <td class="px-4 py-3">
316
+ <div>
317
+ <div class="font-medium">${user.email}</div>
318
+ <div class="text-sm text-gray-500">ID: ${user.id}</div>
319
+ </div>
320
+ </td>
321
+ <td class="px-4 py-3 text-sm text-gray-600">
322
+ ${formatDate(user.created_at)}
323
+ </td>
324
+ <td class="px-4 py-3">
325
+ <div class="text-sm">
326
+ <div>${user.session_count} sessions</div>
327
+ <div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
328
+ </div>
329
+ </td>
330
+ <td class="px-4 py-3">
331
+ <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'}">
332
+ ${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
333
+ </span>
334
+ </td>
335
+ <td class="px-4 py-3">
336
+ <div class="flex space-x-2">
337
+ <button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
338
+ <button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
339
+ </div>
340
+ </td>
341
+ </tr>
342
+ `).join('');
343
+ }
344
+
345
+ function updateUsersPagination(data) {
346
+ document.getElementById('usersInfo').textContent =
347
+ `Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
348
+
349
+ document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
350
+
351
+ document.getElementById('prevPageBtn').disabled = data.page <= 1;
352
+ document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
353
+ }
354
+
355
+ async function viewUser(userId) {
356
+ try {
357
+ const response = await fetch(`/admin/users/${userId}`);
358
+ const data = await response.json();
359
+
360
+ if (data.success) {
361
+ showUserModal(data.user);
362
+ }
363
+ } catch (error) {
364
+ console.error('User details error:', error);
365
+ showToast('Failed to load user details', 'error');
366
+ }
367
+ }
368
+
369
+ function showUserModal(user) {
370
+ const modal = document.getElementById('userDetailModal');
371
+ const content = document.getElementById('userDetailContent');
372
+
373
+ content.innerHTML = `
374
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
+ <div>
376
+ <h4 class="font-semibold mb-3">📋 Basic Information</h4>
377
+ <div class="space-y-2 text-sm">
378
+ <div><strong>Email:</strong> ${user.user.email}</div>
379
+ <div><strong>ID:</strong> ${user.user.id}</div>
380
+ <div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
381
+ <div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
382
+ <div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
383
+ </div>
384
+ </div>
385
+
386
+ <div>
387
+ <h4 class="font-semibold mb-3">⚙️ Settings</h4>
388
+ <div class="space-y-2 text-sm">
389
+ ${Object.entries(user.settings).map(([key, value]) =>
390
+ `<div><strong>${key}:</strong> ${value}</div>`
391
+ ).join('')}
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="mt-6">
397
+ <h4 class="font-semibold mb-3">📊 Recent Activity</h4>
398
+ <div class="max-h-48 overflow-y-auto">
399
+ ${user.recent_sessions.map(session => `
400
+ <div class="flex justify-between py-2 border-b text-sm">
401
+ <span>${session.activity}</span>
402
+ <span class="text-gray-500">${formatDate(session.timestamp)}</span>
403
+ </div>
404
+ `).join('')}
405
+ </div>
406
+ </div>
407
+
408
+ <div class="mt-6">
409
+ <h4 class="font-semibold mb-3">🎯 Token Usage</h4>
410
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
411
+ ${user.token_usage.map(usage => `
412
+ <div class="bg-gray-50 p-3 rounded">
413
+ <div class="font-medium">${usage.provider}</div>
414
+ <div class="text-sm text-gray-600">
415
+ Input: ${usage.input_tokens.toLocaleString()}<br>
416
+ Output: ${usage.output_tokens.toLocaleString()}<br>
417
+ Calls: ${usage.calls}
418
+ </div>
419
+ </div>
420
+ `).join('')}
421
+ </div>
422
+ </div>
423
+ `;
424
+
425
+ modal.classList.remove('hidden');
426
+ }
427
+
428
+ function closeUserModal() {
429
+ document.getElementById('userDetailModal').classList.add('hidden');
430
+ }
431
+
432
+ async function deleteUser(userId, email) {
433
+ if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
434
+ return;
435
+ }
436
+
437
+ try {
438
+ const response = await fetch(`/admin/users/${userId}`, {
439
+ method: 'DELETE'
440
+ });
441
+
442
+ const data = await response.json();
443
+
444
+ if (data.success) {
445
+ showToast('User deleted successfully', 'success');
446
+ loadUsers(); // Refresh the table
447
+ } else {
448
+ showToast('Failed to delete user', 'error');
449
+ }
450
+ } catch (error) {
451
+ console.error('Delete user error:', error);
452
+ showToast('Failed to delete user', 'error');
453
+ }
454
+ }
455
+
456
+ async function loadDatabaseSchema() {
457
+ try {
458
+ const response = await fetch('/admin/database/schema');
459
+ const data = await response.json();
460
+
461
+ if (data.success) {
462
+ updateDatabaseSchema(data.schema);
463
+ }
464
+ } catch (error) {
465
+ console.error('Schema loading error:', error);
466
+ showToast('Failed to load database schema', 'error');
467
+ }
468
+ }
469
+
470
+ function updateDatabaseSchema(schema) {
471
+ const container = document.getElementById('databaseSchema');
472
+
473
+ container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
474
+ <div class="border rounded-lg p-4">
475
+ <div class="flex justify-between items-center mb-3">
476
+ <h5 class="font-semibold text-lg">${tableName}</h5>
477
+ <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
478
+ ${tableInfo.row_count} rows
479
+ </span>
480
+ </div>
481
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
482
+ ${tableInfo.columns.map(col => `
483
+ <div class="flex items-center space-x-2">
484
+ <span class="font-medium">${col.name}</span>
485
+ <span class="text-gray-500">${col.type}</span>
486
+ ${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
487
+ ${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
488
+ </div>
489
+ `).join('')}
490
+ </div>
491
+ </div>
492
+ `).join('');
493
+ }
494
+
495
+ async function loadTokenUsage() {
496
+ try {
497
+ // For now, create sample charts with existing data
498
+ await createTokenUsageCharts();
499
+ } catch (error) {
500
+ console.error('Token usage loading error:', error);
501
+ showToast('Failed to load token usage data', 'error');
502
+ }
503
+ }
504
+
505
+ async function createTokenUsageCharts() {
506
+ // Provider Usage Chart
507
+ const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
508
+
509
+ new Chart(providerCtx, {
510
+ type: 'bar',
511
+ data: {
512
+ labels: ['Groq', 'Gemini'],
513
+ datasets: [{
514
+ label: 'Input Tokens',
515
+ data: [150000, 85000],
516
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
517
+ borderColor: 'rgba(59, 130, 246, 1)',
518
+ borderWidth: 1
519
+ }, {
520
+ label: 'Output Tokens',
521
+ data: [45000, 28000],
522
+ backgroundColor: 'rgba(147, 51, 234, 0.8)',
523
+ borderColor: 'rgba(147, 51, 234, 1)',
524
+ borderWidth: 1
525
+ }]
526
+ },
527
+ options: {
528
+ responsive: true,
529
+ plugins: {
530
+ title: {
531
+ display: true,
532
+ text: 'Token Usage by Provider'
533
+ }
534
+ },
535
+ scales: {
536
+ y: {
537
+ beginAtZero: true,
538
+ title: {
539
+ display: true,
540
+ text: 'Tokens'
541
+ }
542
+ }
543
+ }
544
+ }
545
+ });
546
+
547
+ // Cost Breakdown Chart
548
+ const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
549
+
550
+ new Chart(costCtx, {
551
+ type: 'pie',
552
+ data: {
553
+ labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
554
+ datasets: [{
555
+ data: [40, 25, 20, 15],
556
+ backgroundColor: [
557
+ 'rgba(34, 197, 94, 0.8)',
558
+ 'rgba(59, 130, 246, 0.8)',
559
+ 'rgba(147, 51, 234, 0.8)',
560
+ 'rgba(251, 146, 60, 0.8)'
561
+ ],
562
+ borderColor: [
563
+ 'rgba(34, 197, 94, 1)',
564
+ 'rgba(59, 130, 246, 1)',
565
+ 'rgba(147, 51, 234, 1)',
566
+ 'rgba(251, 146, 60, 1)'
567
+ ],
568
+ borderWidth: 2
569
+ }]
570
+ },
571
+ options: {
572
+ responsive: true,
573
+ plugins: {
574
+ title: {
575
+ display: true,
576
+ text: 'Cost Distribution by Operation Type'
577
+ },
578
+ legend: {
579
+ position: 'bottom'
580
+ }
581
+ }
582
+ }
583
+ });
584
+ }
585
+
586
+ function refreshUsers() {
587
+ currentPage = 1;
588
+ loadUsers();
589
+ }
590
+
591
+ function previousPage() {
592
+ if (currentPage > 1) {
593
+ currentPage--;
594
+ loadUsers();
595
+ }
596
+ }
597
+
598
+ function nextPage() {
599
+ if (usersData && currentPage < usersData.total_pages) {
600
+ currentPage++;
601
+ loadUsers();
602
+ }
603
+ }
604
+
605
+ function createCharts(stats) {
606
+ createUserGrowthChart(stats);
607
+ createTokenCostChart(stats);
608
+ }
609
+
610
+ function createUserGrowthChart(stats) {
611
+ const ctx = document.getElementById('userGrowthChart').getContext('2d');
612
+
613
+ // Generate sample data for last 30 days
614
+ const dates = [];
615
+ const userData = [];
616
+ const today = new Date();
617
+
618
+ for (let i = 29; i >= 0; i--) {
619
+ const date = new Date(today);
620
+ date.setDate(date.getDate() - i);
621
+ dates.push(date.toLocaleDateString());
622
+
623
+ // Simulate growth data (would come from real analytics)
624
+ userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
625
+ }
626
+
627
+ new Chart(ctx, {
628
+ type: 'line',
629
+ data: {
630
+ labels: dates,
631
+ datasets: [{
632
+ label: 'Total Users',
633
+ data: userData,
634
+ borderColor: 'rgb(59, 130, 246)',
635
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
636
+ fill: true,
637
+ tension: 0.4
638
+ }]
639
+ },
640
+ options: {
641
+ responsive: true,
642
+ plugins: {
643
+ title: {
644
+ display: true,
645
+ text: 'User Growth Over Time'
646
+ }
647
+ },
648
+ interaction: {
649
+ intersect: false,
650
+ },
651
+ scales: {
652
+ x: {
653
+ display: true,
654
+ title: {
655
+ display: true,
656
+ text: 'Date'
657
+ }
658
+ },
659
+ y: {
660
+ display: true,
661
+ title: {
662
+ display: true,
663
+ text: 'Users'
664
+ }
665
+ }
666
+ }
667
+ }
668
+ });
669
+ }
670
+
671
+ function createTokenCostChart(stats) {
672
+ const ctx = document.getElementById('tokenCostChart').getContext('2d');
673
+
674
+ // Sample cost data
675
+ const providers = ['Groq', 'Gemini'];
676
+ const costs = [
677
+ (stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
678
+ (stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
679
+ ];
680
+
681
+ new Chart(ctx, {
682
+ type: 'doughnut',
683
+ data: {
684
+ labels: providers,
685
+ datasets: [{
686
+ label: 'Cost ($)',
687
+ data: costs,
688
+ backgroundColor: [
689
+ 'rgba(59, 130, 246, 0.8)',
690
+ 'rgba(147, 51, 234, 0.8)'
691
+ ],
692
+ borderColor: [
693
+ 'rgba(59, 130, 246, 1)',
694
+ 'rgba(147, 51, 234, 1)'
695
+ ],
696
+ borderWidth: 2
697
+ }]
698
+ },
699
+ options: {
700
+ responsive: true,
701
+ plugins: {
702
+ title: {
703
+ display: true,
704
+ text: 'API Costs by Provider'
705
+ },
706
+ legend: {
707
+ position: 'bottom',
708
+ }
709
+ }
710
+ }
711
+ });
712
+ }
713
+
714
+ function exportUsers() {
715
+ window.open('/admin/export/users', '_blank');
716
+ showToast('Users data export started', 'success');
717
+ }
718
+
719
+ function exportTokens() {
720
+ window.open('/admin/export/tokens', '_blank');
721
+ showToast('Token usage data export started', 'success');
722
+ }
723
+
724
+ async function loadSystemHealth() {
725
+ try {
726
+ // Load system health metrics
727
+ const healthResponse = await fetch('/admin/system/health');
728
+ const healthData = await healthResponse.json();
729
+
730
+ if (healthData.success) {
731
+ updateSystemMetrics(healthData.health);
732
+ }
733
+
734
+ // Load system alerts
735
+ const alertsResponse = await fetch('/admin/system/alerts');
736
+ const alertsData = await alertsResponse.json();
737
+
738
+ if (alertsData.success) {
739
+ updateSystemAlerts(alertsData.alerts);
740
+ }
741
+ } catch (error) {
742
+ console.error('System health loading error:', error);
743
+ showToast('Failed to load system health data', 'error');
744
+ }
745
+ }
746
+
747
+ function updateSystemMetrics(health) {
748
+ // Memory metrics
749
+ if (health.memory) {
750
+ const memoryPercent = health.memory.percent || 0;
751
+ const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
752
+ const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
753
+
754
+ document.getElementById('memoryMetrics').innerHTML = `
755
+ <div class="space-y-2">
756
+ <div class="flex justify-between">
757
+ <span>Used:</span>
758
+ <span>${memoryUsed}GB / ${memoryTotal}GB</span>
759
+ </div>
760
+ <div class="w-full bg-gray-200 rounded-full h-2">
761
+ <div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
762
+ </div>
763
+ <div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
764
+ </div>
765
+ `;
766
+ }
767
+
768
+ // Disk metrics
769
+ if (health.disk) {
770
+ const diskPercent = health.disk.percent || 0;
771
+ const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
772
+ const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
773
+
774
+ document.getElementById('diskMetrics').innerHTML = `
775
+ <div class="space-y-2">
776
+ <div class="flex justify-between">
777
+ <span>Used:</span>
778
+ <span>${diskUsed}GB / ${diskTotal}GB</span>
779
+ </div>
780
+ <div class="w-full bg-gray-200 rounded-full h-2">
781
+ <div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
782
+ </div>
783
+ <div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
784
+ </div>
785
+ `;
786
+ }
787
+
788
+ // Database metrics
789
+ if (health.database) {
790
+ document.getElementById('databaseMetrics').innerHTML = `
791
+ <div class="space-y-2">
792
+ <div class="flex justify-between">
793
+ <span>Size:</span>
794
+ <span>${health.database.size_mb}MB</span>
795
+ </div>
796
+ <div class="flex justify-between">
797
+ <span>Uptime:</span>
798
+ <span>${health.uptime || 'Unknown'}</span>
799
+ </div>
800
+ </div>
801
+ `;
802
+ }
803
+
804
+ // Error logs
805
+ if (health.recent_errors && health.recent_errors.length > 0) {
806
+ document.getElementById('errorLogs').innerHTML = `
807
+ <div class="space-y-2 max-h-64 overflow-y-auto">
808
+ ${health.recent_errors.map(error => `
809
+ <div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
810
+ <div class="flex justify-between items-start">
811
+ <span class="font-medium text-red-800">${error.level}</span>
812
+ <span class="text-red-600 text-xs">${error.timestamp}</span>
813
+ </div>
814
+ <div class="text-red-700 mt-1">${error.message}</div>
815
+ <div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
816
+ </div>
817
+ `).join('')}
818
+ </div>
819
+ `;
820
+ } else {
821
+ document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
822
+ }
823
+ }
824
+
825
+ function updateSystemAlerts(alerts) {
826
+ const container = document.getElementById('systemAlerts');
827
+
828
+ if (alerts.length === 0) {
829
+ container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
830
+ return;
831
+ }
832
+
833
+ container.innerHTML = alerts.map(alert => {
834
+ const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
835
+ alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
836
+ 'bg-blue-50 border-blue-200';
837
+ const textColor = alert.type === 'error' ? 'text-red-800' :
838
+ alert.type === 'warning' ? 'text-yellow-800' :
839
+ 'text-blue-800';
840
+ const icon = alert.type === 'error' ? '🚨' :
841
+ alert.type === 'warning' ? '⚠️' : 'ℹ️';
842
+
843
+ return `
844
+ <div class="p-3 ${bgColor} border rounded-lg">
845
+ <div class="flex items-start gap-3">
846
+ <span class="text-lg">${icon}</span>
847
+ <div class="flex-1">
848
+ <div class="font-medium ${textColor}">${alert.message}</div>
849
+ <div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
850
+ </div>
851
+ </div>
852
+ </div>
853
+ `;
854
+ }).join('');
855
+ }
856
+
857
+ function formatDate(dateString) {
858
+ if (!dateString) return 'Never';
859
+ return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
860
+ }
861
+
862
+ function showToast(message, type = 'info') {
863
+ const toast = document.createElement('div');
864
+ toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
865
+ type === 'success' ? 'bg-green-500' :
866
+ type === 'error' ? 'bg-red-500' :
867
+ type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
868
+ } text-white`;
869
+
870
+ toast.textContent = message;
871
+ document.body.appendChild(toast);
872
+
873
+ setTimeout(() => {
874
+ toast.remove();
875
+ }, 5000);
876
+ }
877
+
878
+ // Add CSS for tab styling
879
+ const style = document.createElement('style');
880
+ style.textContent = `
881
+ .admin-tab {
882
+ padding: 0.5rem 1rem;
883
+ border-bottom: 2px solid transparent;
884
+ font-medium: 500;
885
+ text-decoration: none;
886
+ transition: all 0.2s;
887
+ }
888
+ .admin-tab:hover {
889
+ text-decoration: none;
890
+ border-bottom-color: #d1d5db;
891
+ }
892
+ .admin-tab.active {
893
+ border-bottom-color: #3b82f6;
894
+ color: #3b82f6;
895
+ }
896
+ `;
897
+ document.head.appendChild(style);
898
+ </script>
899
+ </body>
900
  </html>
templates/index.html CHANGED
The diff for this file is too large to render. See raw diff