amauricunha commited on
Commit
9e45e09
·
verified ·
1 Parent(s): e52b9d8

Upload 3 files

Browse files
Files changed (3) hide show
  1. templates/admin.html +900 -1
  2. templates/base.html +900 -0
  3. templates/index.html +0 -0
templates/admin.html CHANGED
@@ -1 +1,900 @@
1
- t
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +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>
templates/index.html ADDED
The diff for this file is too large to render. See raw diff