alphabagibagi commited on
Commit
807e987
·
verified ·
1 Parent(s): 834c5b2

Upload 3 files

Browse files
Files changed (3) hide show
  1. admin.py +455 -0
  2. requirements.txt +6 -0
  3. start_admin.sh +3 -0
admin.py ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from datetime import datetime, timezone, timedelta
4
+ from flask import Flask, render_template_string, request, redirect, url_for, flash, session, jsonify
5
+ from functools import wraps
6
+ from supabase import create_client, Client
7
+ from dotenv import load_dotenv
8
+ import requests
9
+
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ # Configuration
14
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
15
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
16
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
17
+ FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "icebox-admin-secret-777")
18
+
19
+ # Initialize Supabase Client
20
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
21
+
22
+ app = Flask(__name__)
23
+ app.secret_key = FLASK_SECRET_KEY
24
+
25
+ # Authentication Decorator
26
+ def login_required(f):
27
+ @wraps(f)
28
+ def decorated_function(*args, **kwargs):
29
+ if 'logged_in' not in session:
30
+ return redirect(url_for('login'))
31
+ return f(*args, **kwargs)
32
+ return decorated_function
33
+
34
+ # Templates
35
+ LOGIN_TEMPLATE = """
36
+ <!DOCTYPE html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="UTF-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
41
+ <title>Admin Login | Icebox AI</title>
42
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
43
+ <style>
44
+ :root {
45
+ --primary-bg: #0f172a;
46
+ --accent: #6366f1;
47
+ --glass: rgba(255, 255, 255, 0.05);
48
+ }
49
+ body {
50
+ background-color: var(--primary-bg);
51
+ color: white;
52
+ height: 100vh;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ font-family: 'Inter', sans-serif;
57
+ }
58
+ .login-card {
59
+ background: var(--glass);
60
+ backdrop-filter: blur(12px);
61
+ border: 1px solid rgba(255, 255, 255, 0.1);
62
+ border-radius: 20px;
63
+ padding: 40px;
64
+ width: 100%;
65
+ max-width: 400px;
66
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
67
+ }
68
+ .form-control {
69
+ background: rgba(255, 255, 255, 0.05);
70
+ border: 1px solid rgba(255, 255, 255, 0.1);
71
+ color: white;
72
+ border-radius: 10px;
73
+ padding: 12px;
74
+ }
75
+ .form-control:focus {
76
+ background: rgba(255, 255, 255, 0.1);
77
+ border-color: var(--accent);
78
+ color: white;
79
+ box-shadow: none;
80
+ }
81
+ .btn-primary {
82
+ background: var(--accent);
83
+ border: none;
84
+ border-radius: 10px;
85
+ padding: 12px;
86
+ font-weight: 600;
87
+ }
88
+ .logo {
89
+ text-align: center;
90
+ margin-bottom: 30px;
91
+ }
92
+ .logo h1 {
93
+ font-size: 24px;
94
+ font-weight: 800;
95
+ background: linear-gradient(to right, #818cf8, #c084fc);
96
+ -webkit-background-clip: text;
97
+ -webkit-text-fill-color: transparent;
98
+ }
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <div class="login-card">
103
+ <div class="logo">
104
+ <h1>ICEBOX AI ADMIN</h1>
105
+ <p class="text-secondary">Control Center</p>
106
+ </div>
107
+ {% with messages = get_flashed_messages() %}
108
+ {% if messages %}
109
+ {% for message in messages %}
110
+ <div class="alert alert-danger py-2" role="alert" style="background: rgba(220, 53, 69, 0.2); border: none; color: #ff8787;">
111
+ {{ message }}
112
+ </div>
113
+ {% endfor %}
114
+ {% endif %}
115
+ {% endwith %}
116
+ <form method="POST">
117
+ <div class="mb-4">
118
+ <label class="form-label text-secondary small">ACCESS TOKEN</label>
119
+ <input type="password" name="password" class="form-control" placeholder="••••••••" required>
120
+ </div>
121
+ <button type="submit" class="btn btn-primary w-100">AUTHENTICATE</button>
122
+ </form>
123
+ </div>
124
+ </body>
125
+ </html>
126
+ """
127
+
128
+ INDEX_TEMPLATE = """
129
+ <!DOCTYPE html>
130
+ <html lang="en">
131
+ <head>
132
+ <meta charset="UTF-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
+ <title>Dashboard | Icebox AI Admin</title>
135
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
136
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
137
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
138
+ <style>
139
+ :root {
140
+ --bg: #0b0f19;
141
+ --sidebar: #111827;
142
+ --card: #1f2937;
143
+ --accent: #6366f1;
144
+ --text-muted: #9ca3af;
145
+ }
146
+ body {
147
+ background-color: var(--bg);
148
+ color: #f3f4f6;
149
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
150
+ overflow-x: hidden;
151
+ }
152
+ .sidebar {
153
+ width: 250px;
154
+ background-color: var(--sidebar);
155
+ height: 100vh;
156
+ position: fixed;
157
+ padding: 20px;
158
+ border-right: 1px solid rgba(255, 255, 255, 0.05);
159
+ }
160
+ .main-content {
161
+ margin-left: 250px;
162
+ padding: 30px;
163
+ }
164
+ .nav-link {
165
+ color: var(--text-muted);
166
+ padding: 12px 15px;
167
+ border-radius: 10px;
168
+ margin-bottom: 5px;
169
+ transition: 0.3s;
170
+ }
171
+ .nav-link:hover, .nav-link.active {
172
+ color: white;
173
+ background: rgba(99, 102, 241, 0.1);
174
+ }
175
+ .nav-link i {
176
+ margin-right: 10px;
177
+ }
178
+ .stat-card {
179
+ background: var(--card);
180
+ border-radius: 15px;
181
+ padding: 24px;
182
+ border: 1px solid rgba(255, 255, 255, 0.05);
183
+ height: 100%;
184
+ }
185
+ .stat-icon {
186
+ width: 48px;
187
+ height: 48px;
188
+ border-radius: 12px;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ font-size: 24px;
193
+ margin-bottom: 15px;
194
+ }
195
+ .table-container {
196
+ background: var(--card);
197
+ border-radius: 15px;
198
+ padding: 20px;
199
+ border: 1px solid rgba(255, 255, 255, 0.05);
200
+ margin-top: 25px;
201
+ }
202
+ .table {
203
+ color: #e5e7eb;
204
+ vertical-align: middle;
205
+ }
206
+ .table thead th {
207
+ color: var(--text-muted);
208
+ font-weight: 500;
209
+ text-transform: uppercase;
210
+ font-size: 11px;
211
+ letter-spacing: 0.05em;
212
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
213
+ }
214
+ .table td {
215
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
216
+ padding: 15px 10px;
217
+ }
218
+ .badge-premium { background: rgba(168, 85, 247, 0.2); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
219
+ .badge-free { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
220
+
221
+ .search-bar {
222
+ background: rgba(255, 255, 255, 0.05);
223
+ border: 1px solid rgba(255, 255, 255, 0.1);
224
+ border-radius: 10px;
225
+ color: white;
226
+ padding: 10px 15px;
227
+ }
228
+ </style>
229
+ </head>
230
+ <body>
231
+ <div class="sidebar">
232
+ <h4 class="fw-bold mb-4 px-3" style="color: var(--accent);">Icebox Admin</h4>
233
+ <nav class="nav flex-column">
234
+ <a class="nav-link active" href="/"><i class="bi bi-grid-1x2"></i> Overview</a>
235
+ <a class="nav-link" href="#users"><i class="bi bi-people"></i> Users</a>
236
+ <a class="nav-link" href="#generations"><i class="bi bi-robot"></i> Generations</a>
237
+ <hr class="opacity-10 my-4">
238
+ <a class="nav-link text-danger" href="/logout"><i class="bi bi-box-arrow-left"></i> Logout</a>
239
+ </nav>
240
+ </div>
241
+
242
+ <div class="main-content">
243
+ <div class="d-flex justify-content-between align-items-center mb-5">
244
+ <div>
245
+ <h2 class="fw-bold mb-0">Platform Analytics</h2>
246
+ <p class="text-secondary small">Real-time update as of {{ now }}</p>
247
+ </div>
248
+ <div class="d-flex gap-3">
249
+ <input type="text" class="search-bar" placeholder="Search users...">
250
+ <button class="btn btn-primary px-4"><i class="bi bi-arrow-clockwise"></i></button>
251
+ </div>
252
+ </div>
253
+
254
+ <div class="row g-4">
255
+ <div class="col-md-3">
256
+ <div class="stat-card">
257
+ <div class="stat-icon" style="background: rgba(59, 130, 246, 0.1); color: #3b82f6;">
258
+ <i class="bi bi-people"></i>
259
+ </div>
260
+ <div class="text-secondary small fw-medium">TOTAL USERS</div>
261
+ <h3 class="fw-bold mt-1">{{ stats.total_users }}</h3>
262
+ </div>
263
+ </div>
264
+ <div class="col-md-3">
265
+ <div class="stat-card">
266
+ <div class="stat-icon" style="background: rgba(16, 185, 129, 0.1); color: #10b981;">
267
+ <i class="bi bi-image"></i>
268
+ </div>
269
+ <div class="text-secondary small fw-medium">TOTAL IMAGES</div>
270
+ <h3 class="fw-bold mt-1">{{ stats.total_images }}</h3>
271
+ </div>
272
+ </div>
273
+ <div class="col-md-3">
274
+ <div class="stat-card">
275
+ <div class="stat-icon" style="background: rgba(245, 158, 11, 0.1); color: #f59e0b;">
276
+ <i class="bi bi-mic"></i>
277
+ </div>
278
+ <div class="text-secondary small fw-medium">TOTAL VOICES</div>
279
+ <h3 class="fw-bold mt-1">{{ stats.total_voices }}</h3>
280
+ </div>
281
+ </div>
282
+ <div class="col-md-3">
283
+ <div class="stat-card">
284
+ <div class="stat-icon" style="background: rgba(139, 92, 246, 0.1); color: #8b5cf6;">
285
+ <i class="bi bi-gem"></i>
286
+ </div>
287
+ <div class="text-secondary small fw-medium">PREMIUM USERS</div>
288
+ <h3 class="fw-bold mt-1">{{ stats.premium_users }}</h3>
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="row mt-4 g-4">
294
+ <div class="col-md-8">
295
+ <div class="table-container h-100">
296
+ <div class="d-flex justify-content-between align-items-center mb-4">
297
+ <h5 class="fw-bold mb-0">Recent Users</h5>
298
+ <a href="#" class="text-accent small text-decoration-none">View All</a>
299
+ </div>
300
+ <div class="table-responsive">
301
+ <table class="table">
302
+ <thead>
303
+ <tr>
304
+ <th>User</th>
305
+ <th>Chat ID</th>
306
+ <th>Tier</th>
307
+ <th>Activity</th>
308
+ <th>Action</th>
309
+ </tr>
310
+ </thead>
311
+ <tbody>
312
+ {% for user in users %}
313
+ <tr>
314
+ <td>
315
+ <div class="d-flex align-items-center">
316
+ <div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3" style="width: 35px; height: 35px; font-size: 14px;">
317
+ {{ user.first_name[0] if user.first_name else 'U' }}
318
+ </div>
319
+ <div>
320
+ <div class="fw-medium">{{ user.first_name or 'User' }}</div>
321
+ <div class="text-secondary x-small">@{{ user.username or 'unknown' }}</div>
322
+ </div>
323
+ </div>
324
+ </td>
325
+ <td class="small">{{ user.chat_id }}</td>
326
+ <td>
327
+ <span class="badge {{ 'badge-premium' if user.tier == 'paid' else 'badge-free' }}">
328
+ {{ user.tier.upper() }}
329
+ </span>
330
+ </td>
331
+ <td>
332
+ <div class="text-secondary small">Img: {{ user.total_images_generated }}</div>
333
+ <div class="text-secondary small">Token: {{ user.token_balance }}</div>
334
+ </td>
335
+ <td>
336
+ <button class="btn btn-sm btn-outline-secondary border-0" onclick="editUser('{{ user.chat_id }}')">
337
+ <i class="bi bi-pencil-square"></i>
338
+ </button>
339
+ </td>
340
+ </tr>
341
+ {% endfor %}
342
+ </tbody>
343
+ </table>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ <div class="col-md-4">
348
+ <div class="table-container h-100">
349
+ <h5 class="fw-bold mb-4">Activity Mix</h5>
350
+ <canvas id="activityChart" height="300"></canvas>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <script>
357
+ const ctx = document.getElementById('activityChart').getContext('2d');
358
+ new Chart(ctx, {
359
+ type: 'doughnut',
360
+ data: {
361
+ labels: ['Images', 'Voices'],
362
+ datasets: [{
363
+ data: [{{ stats.total_images }}, {{ stats.total_voices }}],
364
+ backgroundColor: ['#6366f1', '#f59e0b'],
365
+ borderWidth: 0,
366
+ hoverOffset: 4
367
+ }]
368
+ },
369
+ options: {
370
+ plugins: {
371
+ legend: {
372
+ position: 'bottom',
373
+ labels: { color: '#9ca3af', padding: 20 }
374
+ }
375
+ },
376
+ cutout: '70%'
377
+ }
378
+ });
379
+
380
+ function editUser(chat_id) {
381
+ alert('User management for ' + chat_id + ' coming soon in next update');
382
+ }
383
+ </script>
384
+ </body>
385
+ </html>
386
+ """
387
+
388
+ # Routes
389
+ @app.route('/login', methods=['GET', 'POST'])
390
+ def login():
391
+ if request.method == 'POST':
392
+ password = request.form.get('password')
393
+ if password == ADMIN_PASSWORD:
394
+ session['logged_in'] = True
395
+ return redirect(url_for('index'))
396
+ else:
397
+ flash('Invalid access token')
398
+ return render_template_string(LOGIN_TEMPLATE)
399
+
400
+ @app.route('/logout')
401
+ def logout():
402
+ session.pop('logged_in', None)
403
+ return redirect(url_for('login'))
404
+
405
+ @app.route('/')
406
+ @login_required
407
+ def index():
408
+ try:
409
+ # Fetch stats
410
+ users_res = supabase.table("telegram_users").select("*", count="exact").order("created_at", desc=True).limit(50).execute()
411
+
412
+ # Aggregate stats
413
+ total_users = users_res.count if users_res.count else 0
414
+
415
+ # Get generation counts using counts from all users (simple aggregation for demo)
416
+ images_gen = 0
417
+ voices_gen = 0
418
+ premium_count = 0
419
+
420
+ # For a real large DB, we should use separate rollup tables or specialized RPCs
421
+ # But for this case, we aggregate from the user management data
422
+ all_users = supabase.table("telegram_users").select("tier, total_images_generated").execute()
423
+ for u in all_users.data:
424
+ images_gen += u.get('total_images_generated', 0)
425
+ if u.get('tier') == 'paid':
426
+ premium_count += 1
427
+
428
+ # Count voices from log table
429
+ voices_count_res = supabase.table("voice_generation_logs").select("id", count="exact").execute()
430
+ voices_gen = voices_count_res.count or 0
431
+
432
+ stats = {
433
+ "total_users": total_users,
434
+ "total_images": images_gen,
435
+ "total_voices": voices_gen,
436
+ "premium_users": premium_count
437
+ }
438
+
439
+ return render_template_string(
440
+ INDEX_TEMPLATE,
441
+ stats=stats,
442
+ users=users_res.data,
443
+ now=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
444
+ )
445
+ except Exception as e:
446
+ return f"Database Error: {str(e)}"
447
+
448
+ # Health check endpoint for Space
449
+ @app.route('/health')
450
+ def health():
451
+ return jsonify({"status": "healthy", "time": datetime.now().isoformat()}), 200
452
+
453
+ if __name__ == "__main__":
454
+ port = int(os.getenv("PORT", 7860))
455
+ app.run(host='0.0.0.0', port=port, debug=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ python-telegram-bot
2
+ supabase
3
+ python-dotenv
4
+ flask
5
+ requests
6
+ APScheduler>=3.10.0
start_admin.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ # Start the Admin Dashboard
3
+ python admin.py