alphabagibagi commited on
Commit
ba3dd2a
·
verified ·
1 Parent(s): dc46c1b

Update admin.py

Browse files
Files changed (1) hide show
  1. admin.py +337 -39
admin.py CHANGED
@@ -31,6 +31,17 @@ def login_required(f):
31
  return f(*args, **kwargs)
32
  return decorated_function
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  # Templates
35
  LOGIN_TEMPLATE = """
36
  <!DOCTYPE html>
@@ -304,6 +315,33 @@ INDEX_TEMPLATE = """
304
  color: var(--text-main);
305
  font-weight: 500;
306
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  </style>
308
  </head>
309
  <body>
@@ -493,18 +531,62 @@ INDEX_TEMPLATE = """
493
 
494
  <div class="row mt-4 g-4">
495
  <div class="col-md-8">
496
- <div class="table-container">
497
- <h5 class="fw-bold mb-4">Activity Insights</h5>
498
- <div class="p-5 text-center text-secondary">
499
- <i class="bi bi-bar-chart-line display-4 d-block mb-3"></i>
500
- Analytics charts and trends will appear here.
 
 
501
  </div>
502
  </div>
503
  </div>
504
  <div class="col-md-4">
505
- <div class="table-container">
506
- <h5 class="fw-bold mb-4">Activity Mix</h5>
507
- <canvas id="activityChart" height="300"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  </div>
509
  </div>
510
  </div>
@@ -632,9 +714,17 @@ INDEX_TEMPLATE = """
632
  <h2 class="fw-bold mb-0">Generation Logs</h2>
633
  <p class="text-secondary small">Latest activity across all bots</p>
634
  </div>
635
- <div class="nav nav-pills" id="gen-tabs">
636
- <button class="nav-link active me-2" data-bs-toggle="pill" data-bs-target="#voice-logs">Voice Logs</button>
637
- <button class="nav-link" data-bs-toggle="pill" data-bs-target="#image-logs">Image Logs</button>
 
 
 
 
 
 
 
 
638
  </div>
639
  </div>
640
 
@@ -821,6 +911,17 @@ INDEX_TEMPLATE = """
821
  window.location.href = url.toString();
822
  }
823
 
 
 
 
 
 
 
 
 
 
 
 
824
  // Selection Logic
825
  function updateSelectionState() {
826
  const checks = document.querySelectorAll('.user-checkbox:checked');
@@ -869,11 +970,19 @@ INDEX_TEMPLATE = """
869
  });
870
  }
871
 
872
- // Chart Initialization (Safe check)
873
- const chartEl = document.getElementById('activityChart');
874
- if (chartEl) {
875
- const ctx = chartEl.getContext('2d');
876
- new Chart(ctx, {
 
 
 
 
 
 
 
 
877
  type: 'doughnut',
878
  data: {
879
  labels: ['Images', 'Voices'],
@@ -888,11 +997,64 @@ INDEX_TEMPLATE = """
888
  plugins: {
889
  legend: {
890
  position: 'bottom',
891
- labels: { color: '#9ca3af', padding: 20 }
892
  }
893
  },
894
  cutout: '70%',
895
- responsive: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  }
897
  });
898
  }
@@ -986,6 +1148,7 @@ INDEX_TEMPLATE = """
986
 
987
  if (data.status === 'success') {
988
  const u = data.user;
 
989
  content.innerHTML = `
990
  <div class="info-item">
991
  <span class="info-label">Username</span>
@@ -995,6 +1158,10 @@ INDEX_TEMPLATE = """
995
  <span class="info-label">Full Name</span>
996
  <span class="info-value">${u.first_name || '-'} ${u.last_name || ''}</span>
997
  </div>
 
 
 
 
998
  <div class="info-item">
999
  <span class="info-label">Chat ID</span>
1000
  <span class="info-value font-monospace">${u.chat_id}</span>
@@ -1004,24 +1171,24 @@ INDEX_TEMPLATE = """
1004
  <span class="info-value">${u.language_code || 'N/A'}</span>
1005
  </div>
1006
  <div class="info-item">
1007
- <span class="info-label">Daily Total Generated</span>
1008
- <span class="info-value">${(u.daily_images_generated || 0) + (u.daily_voices_generated || 0)}</span>
1009
  </div>
1010
  <div class="info-item">
1011
  <span class="info-label">Total Generated</span>
1012
- <span class="info-value">${(u.total_images_generated || 0) + (u.total_voices_generated || 0)}</span>
1013
  </div>
1014
  <div class="info-item">
1015
  <span class="info-label">Images (Total / Daily)</span>
1016
- <span class="info-value">${u.total_images_generated || 0} / ${u.daily_images_generated || 0}</span>
1017
  </div>
1018
  <div class="info-item">
1019
  <span class="info-label">Voices (Total / Daily)</span>
1020
- <span class="info-value">${u.total_voices_generated || 0} / ${u.daily_voices_generated || 0}</span>
1021
  </div>
1022
  <div class="info-item">
1023
  <span class="info-label">Remaining Coins</span>
1024
- <span class="info-value fw-bold text-accent">${u.token_balance || 0}</span>
1025
  </div>
1026
  <div class="info-item">
1027
  <span class="info-label">Account Tier</span>
@@ -1029,7 +1196,7 @@ INDEX_TEMPLATE = """
1029
  </div>
1030
  <div class="info-item col-12">
1031
  <span class="info-label">Bots Joined</span>
1032
- <span class="info-value">${u.bots_joined ? u.bots_joined.join(', ') : 'None'}</span>
1033
  </div>
1034
  <div class="info-item">
1035
  <span class="info-label">Joined Date</span>
@@ -1084,11 +1251,60 @@ def logout():
1084
  @login_required
1085
  def api_user_details(chat_id):
1086
  try:
1087
- res = supabase.table("telegram_users").select("*").eq("chat_id", chat_id).execute()
1088
- if res.data:
1089
- return jsonify({"status": "success", "user": res.data[0]})
1090
- return jsonify({"status": "error", "message": "User not found"}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
  except Exception as e:
 
 
1092
  return jsonify({"status": "error", "message": str(e)}), 500
1093
 
1094
  @app.route('/api/admin/save-settings', methods=['POST'])
@@ -1151,34 +1367,105 @@ def index():
1151
  users_res = user_query.range(u_start, u_end).execute()
1152
  total_users_filtered = users_res.count or 0
1153
 
1154
- # Stats (Global)
1155
- stats_query = supabase.table("telegram_users").select("total_images_generated, tier").execute()
1156
- total_images_gen = sum(u.get('total_images_generated', 0) for u in stats_query.data)
1157
- premium_count = sum(1 for u in stats_query.data if u.get('tier') == 'paid')
1158
- total_users_count = len(stats_query.data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1159
 
1160
  # 2. Fetch Voice Logs with Pagination
 
 
 
 
1161
  v_start = (v_page - 1) * per_page
1162
  v_end = v_start + per_page - 1
1163
- voices_logs_res = supabase.table("voice_generation_logs").select("*", count="exact").order("created_at", desc=True).range(v_start, v_end).execute()
1164
  total_voices_count = voices_logs_res.count or 0
1165
 
1166
  # 3. Fetch Image Logs with Pagination
 
 
 
 
1167
  i_start = (i_page - 1) * per_page
1168
  i_end = i_start + per_page - 1
1169
  try:
1170
- image_logs_res = supabase.table("image_generation_logs").select("*", count="exact").order("created_at", desc=True).range(i_start, i_end).execute()
1171
  image_logs_data = image_logs_res.data
1172
  total_images_logs_count = image_logs_res.count or 0
1173
- except:
 
1174
  image_logs_data = []
1175
  total_images_logs_count = 0
1176
 
 
 
 
 
 
 
1177
  stats = {
1178
  "total_users": total_users_count,
1179
  "total_images": total_images_gen,
1180
  "total_voices": total_voices_count,
1181
- "premium_users": premium_count
 
 
 
 
 
 
 
1182
  }
1183
 
1184
  pages = {
@@ -1189,10 +1476,21 @@ def index():
1189
  "i_current": i_page,
1190
  "i_total": (total_images_logs_count + per_page - 1) // per_page,
1191
  "search": u_search,
 
1192
  "bot_filter": bot_filter,
1193
  "per_page": per_page
1194
  }
1195
 
 
 
 
 
 
 
 
 
 
 
1196
  return render_template_string(
1197
  INDEX_TEMPLATE,
1198
  stats=stats,
@@ -1201,7 +1499,7 @@ def index():
1201
  image_logs=image_logs_data,
1202
  settings=settings,
1203
  pages=pages,
1204
- now=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1205
  )
1206
  except Exception as e:
1207
  import traceback
 
31
  return f(*args, **kwargs)
32
  return decorated_function
33
 
34
+ def to_wib(dt_str):
35
+ if not dt_str: return "-"
36
+ try:
37
+ # Supabase strings: '2026-03-03T08:24:15.123+00:00' or similar
38
+ t_part = dt_str.split('.')[0].replace('Z', '').replace('T', ' ')
39
+ dt = datetime.fromisoformat(t_part).replace(tzinfo=timezone.utc)
40
+ wib = dt + timedelta(hours=7)
41
+ return wib.strftime("%Y-%m-%d %H:%M:%S")
42
+ except:
43
+ return dt_str
44
+
45
  # Templates
46
  LOGIN_TEMPLATE = """
47
  <!DOCTYPE html>
 
315
  color: var(--text-main);
316
  font-weight: 500;
317
  }
318
+
319
+ /* Insight Cards */
320
+ .insight-row {
321
+ display: grid;
322
+ grid-template-columns: 1fr 1fr 1fr;
323
+ gap: 15px;
324
+ margin-top: 15px;
325
+ }
326
+ .insight-card {
327
+ background: rgba(255, 255, 255, 0.03);
328
+ border: 1px solid rgba(255, 255, 255, 0.05);
329
+ border-radius: 12px;
330
+ padding: 15px;
331
+ text-align: center;
332
+ }
333
+ .insight-label {
334
+ font-size: 10px;
335
+ color: var(--text-muted);
336
+ text-transform: uppercase;
337
+ letter-spacing: 0.1em;
338
+ margin-bottom: 5px;
339
+ }
340
+ .insight-value {
341
+ font-size: 18px;
342
+ font-weight: 700;
343
+ color: var(--text-main);
344
+ }
345
  </style>
346
  </head>
347
  <body>
 
531
 
532
  <div class="row mt-4 g-4">
533
  <div class="col-md-8">
534
+ <div class="table-container pt-4">
535
+ <div class="d-flex justify-content-between align-items-center mb-4">
536
+ <h5 class="fw-bold mb-0">30-Day Activity Trend</h5>
537
+ <span class="badge bg-dark border border-secondary text-secondary shadow-sm">Traffic & Usage</span>
538
+ </div>
539
+ <div style="height: 350px;">
540
+ <canvas id="trendChart"></canvas>
541
  </div>
542
  </div>
543
  </div>
544
  <div class="col-md-4">
545
+ <div class="table-container pt-4">
546
+ <h5 class="fw-bold mb-4 text-center">Growth Insights</h5>
547
+
548
+ <div class="mb-4">
549
+ <div class="small fw-bold text-secondary mb-2 px-1 text-center">NEW REGISTRATIONS</div>
550
+ <div class="insight-row">
551
+ <div class="insight-card shadow-sm">
552
+ <div class="insight-label">Today</div>
553
+ <div class="insight-value">{{ stats.u_insights.today }}</div>
554
+ </div>
555
+ <div class="insight-card shadow-sm">
556
+ <div class="insight-label">Week</div>
557
+ <div class="insight-value">{{ stats.u_insights.week }}</div>
558
+ </div>
559
+ <div class="insight-card shadow-sm">
560
+ <div class="insight-label">Month</div>
561
+ <div class="insight-value">{{ stats.u_insights.month }}</div>
562
+ </div>
563
+ </div>
564
+ </div>
565
+
566
+ <div class="mb-4">
567
+ <div class="small fw-bold text-secondary mb-2 px-1 text-center">TOTAL GENERATIONS</div>
568
+ <div class="insight-row">
569
+ <div class="insight-card shadow-sm">
570
+ <div class="insight-label">Today</div>
571
+ <div class="insight-value text-accent">{{ stats.g_insights.today }}</div>
572
+ </div>
573
+ <div class="insight-card shadow-sm">
574
+ <div class="insight-label">Week</div>
575
+ <div class="insight-value text-accent">{{ stats.g_insights.week }}</div>
576
+ </div>
577
+ <div class="insight-card shadow-sm">
578
+ <div class="insight-label">Month</div>
579
+ <div class="insight-value text-accent">{{ stats.g_insights.month }}</div>
580
+ </div>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="mt-5">
585
+ <h6 class="fw-bold mb-3 text-center text-muted small">ACTIVITY MIX</h6>
586
+ <div style="height: 200px;">
587
+ <canvas id="activityChart"></canvas>
588
+ </div>
589
+ </div>
590
  </div>
591
  </div>
592
  </div>
 
714
  <h2 class="fw-bold mb-0">Generation Logs</h2>
715
  <p class="text-secondary small">Latest activity across all bots</p>
716
  </div>
717
+ <div class="d-flex align-items-center gap-3">
718
+ <div class="nav nav-pills" id="gen-tabs">
719
+ <button class="nav-link active me-2" data-bs-toggle="pill" data-bs-target="#voice-logs">Voice Logs</button>
720
+ <button class="nav-link" data-bs-toggle="pill" data-bs-target="#image-logs">Image Logs</button>
721
+ </div>
722
+ <div class="position-relative">
723
+ <input type="text" id="genSearchQuery" class="search-bar" placeholder="Search User ID..." value="{{ pages.gen_search or '' }}" style="width: 250px;">
724
+ <button class="btn btn-link position-absolute end-0 top-50 translate-middle-y text-secondary" id="genSearchBtn">
725
+ <i class="bi bi-search"></i>
726
+ </button>
727
+ </div>
728
  </div>
729
  </div>
730
 
 
911
  window.location.href = url.toString();
912
  }
913
 
914
+ function doGenSearch() {
915
+ const query = document.getElementById('genSearchQuery').value;
916
+ const url = new URL(window.location.href);
917
+ if (query) url.searchParams.set('gen_search', query);
918
+ else url.searchParams.delete('gen_search');
919
+ url.searchParams.set('v_page', 1);
920
+ url.searchParams.set('i_page', 1);
921
+ url.hash = '#generations';
922
+ window.location.href = url.toString();
923
+ }
924
+
925
  // Selection Logic
926
  function updateSelectionState() {
927
  const checks = document.querySelectorAll('.user-checkbox:checked');
 
970
  });
971
  }
972
 
973
+ const genSearchBtn = document.getElementById('genSearchBtn');
974
+ const genSearchInput = document.getElementById('genSearchQuery');
975
+ if (genSearchBtn) genSearchBtn.addEventListener('click', doGenSearch);
976
+ if (genSearchInput) {
977
+ genSearchInput.addEventListener('keypress', (e) => {
978
+ if (e.key === 'Enter') doGenSearch();
979
+ });
980
+ }
981
+
982
+ // Summary Chart Initialization
983
+ const summaryCtx = document.getElementById('activityChart');
984
+ if (summaryCtx) {
985
+ new Chart(summaryCtx.getContext('2d'), {
986
  type: 'doughnut',
987
  data: {
988
  labels: ['Images', 'Voices'],
 
997
  plugins: {
998
  legend: {
999
  position: 'bottom',
1000
+ labels: { color: '#9ca3af', boxWidth: 12, padding: 15 }
1001
  }
1002
  },
1003
  cutout: '70%',
1004
+ responsive: true,
1005
+ maintainAspectRatio: false
1006
+ }
1007
+ });
1008
+ }
1009
+
1010
+ // Trend Chart Initialization
1011
+ const trendCtx = document.getElementById('trendChart');
1012
+ if (trendCtx) {
1013
+ new Chart(trendCtx.getContext('2d'), {
1014
+ type: 'line',
1015
+ data: {
1016
+ labels: {{ stats.chart_data.labels | tojson }},
1017
+ datasets: [
1018
+ {
1019
+ label: 'New Users',
1020
+ data: {{ stats.chart_data.users | tojson }},
1021
+ borderColor: '#6366f1',
1022
+ backgroundColor: 'rgba(99, 102, 241, 0.1)',
1023
+ fill: true,
1024
+ tension: 0.4,
1025
+ borderWidth: 2,
1026
+ pointRadius: 0
1027
+ },
1028
+ {
1029
+ label: 'Generations',
1030
+ data: {{ stats.chart_data.gens | tojson }},
1031
+ borderColor: '#10b981',
1032
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1033
+ fill: true,
1034
+ tension: 0.4,
1035
+ borderWidth: 2,
1036
+ pointRadius: 0
1037
+ }
1038
+ ]
1039
+ },
1040
+ options: {
1041
+ responsive: true,
1042
+ maintainAspectRatio: false,
1043
+ plugins: {
1044
+ legend: {
1045
+ labels: { color: '#9ca3af', boxWidth: 12 }
1046
+ }
1047
+ },
1048
+ scales: {
1049
+ y: {
1050
+ grid: { color: 'rgba(255, 255, 255, 0.05)' },
1051
+ ticks: { color: '#9ca3af' }
1052
+ },
1053
+ x: {
1054
+ grid: { display: false },
1055
+ ticks: { color: '#9ca3af', maxRotation: 0 }
1056
+ }
1057
+ }
1058
  }
1059
  });
1060
  }
 
1148
 
1149
  if (data.status === 'success') {
1150
  const u = data.user;
1151
+ const stats = u.computed_stats;
1152
  content.innerHTML = `
1153
  <div class="info-item">
1154
  <span class="info-label">Username</span>
 
1158
  <span class="info-label">Full Name</span>
1159
  <span class="info-value">${u.first_name || '-'} ${u.last_name || ''}</span>
1160
  </div>
1161
+ <div class="info-item">
1162
+ <span class="info-label">User ID (Internal)</span>
1163
+ <span class="info-value font-monospace text-warning">${u.id || 'N/A'}</span>
1164
+ </div>
1165
  <div class="info-item">
1166
  <span class="info-label">Chat ID</span>
1167
  <span class="info-value font-monospace">${u.chat_id}</span>
 
1171
  <span class="info-value">${u.language_code || 'N/A'}</span>
1172
  </div>
1173
  <div class="info-item">
1174
+ <span class="info-label">Daily Generated</span>
1175
+ <span class="info-value">${stats.image_daily + stats.voice_daily}</span>
1176
  </div>
1177
  <div class="info-item">
1178
  <span class="info-label">Total Generated</span>
1179
+ <span class="info-value">${stats.image_total + stats.voice_total}</span>
1180
  </div>
1181
  <div class="info-item">
1182
  <span class="info-label">Images (Total / Daily)</span>
1183
+ <span class="info-value">${stats.image_total} / ${stats.image_daily}</span>
1184
  </div>
1185
  <div class="info-item">
1186
  <span class="info-label">Voices (Total / Daily)</span>
1187
+ <span class="info-value">${stats.voice_total} / ${stats.voice_daily}</span>
1188
  </div>
1189
  <div class="info-item">
1190
  <span class="info-label">Remaining Coins</span>
1191
+ <span class="info-value fw-bold text-accent">${stats.remaining_coins}</span>
1192
  </div>
1193
  <div class="info-item">
1194
  <span class="info-label">Account Tier</span>
 
1196
  </div>
1197
  <div class="info-item col-12">
1198
  <span class="info-label">Bots Joined</span>
1199
+ <span class="info-value">${u.bots_joined ? (Array.isArray(u.bots_joined) ? u.bots_joined.join(', ') : u.bots_joined) : 'None'}</span>
1200
  </div>
1201
  <div class="info-item">
1202
  <span class="info-label">Joined Date</span>
 
1251
  @login_required
1252
  def api_user_details(chat_id):
1253
  try:
1254
+ import re
1255
+ # Detect UUID vs ChatID (BigInt)
1256
+ is_uuid = bool(re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', chat_id.lower()))
1257
+
1258
+ if is_uuid:
1259
+ res = supabase.table("telegram_users").select("*").eq("id", chat_id).execute()
1260
+ else:
1261
+ try:
1262
+ res = supabase.table("telegram_users").select("*").eq("chat_id", int(chat_id)).execute()
1263
+ except:
1264
+ return jsonify({"status": "error", "message": "Invalid Chat ID format"}), 400
1265
+
1266
+ if not res.data:
1267
+ return jsonify({"status": "error", "message": "User not found"}), 404
1268
+
1269
+ u = res.data[0]
1270
+ user_uuid = u['id']
1271
+ u_chat_id = u['chat_id']
1272
+
1273
+ # Fetch Actual Counts from Logs for accuracy
1274
+ today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
1275
+
1276
+ # Voice Counts
1277
+ voice_total_res = supabase.table("voice_generation_logs").select("id", count="exact").eq("user_id", user_uuid).execute()
1278
+ voice_daily_res = supabase.table("voice_generation_logs").select("id", count="exact").eq("user_id", user_uuid).gte("created_at", today_start).execute()
1279
+
1280
+ # Image Counts
1281
+ image_total_res = supabase.table("image_generation_logs").select("id", count="exact").eq("user_id", user_uuid).execute()
1282
+ image_daily_res = supabase.table("image_generation_logs").select("id", count="exact").eq("user_id", user_uuid).gte("created_at", today_start).execute()
1283
+
1284
+ # Calculate remaining coins (free level is 60 as per robot info)
1285
+ if u.get('tier') == 'paid':
1286
+ remaining_coins = u.get('token_balance', 0)
1287
+ else:
1288
+ # For free, use daily_images_generated counter (max 60)
1289
+ remaining_coins = max(0, 60 - u.get('daily_images_generated', 0))
1290
+
1291
+ # Convert times to WIB for frontend
1292
+ u['created_at'] = to_wib(u.get('created_at'))
1293
+ u['updated_at'] = to_wib(u.get('updated_at'))
1294
+
1295
+ # Merge custom stats into user object for frontend
1296
+ u['computed_stats'] = {
1297
+ "voice_total": voice_total_res.count or 0,
1298
+ "voice_daily": voice_daily_res.count or 0,
1299
+ "image_total": image_total_res.count or 0,
1300
+ "image_daily": image_daily_res.count or 0,
1301
+ "remaining_coins": remaining_coins
1302
+ }
1303
+
1304
+ return jsonify({"status": "success", "user": u})
1305
  except Exception as e:
1306
+ import traceback
1307
+ print(traceback.format_exc())
1308
  return jsonify({"status": "error", "message": str(e)}), 500
1309
 
1310
  @app.route('/api/admin/save-settings', methods=['POST'])
 
1367
  users_res = user_query.range(u_start, u_end).execute()
1368
  total_users_filtered = users_res.count or 0
1369
 
1370
+ # Insights Calculation
1371
+ now_dt = datetime.now(timezone.utc)
1372
+ today_start = now_dt.replace(hour=0, minute=0, second=0, microsecond=0)
1373
+ week_start = today_start - timedelta(days=7)
1374
+ month_start = today_start - timedelta(days=30)
1375
+
1376
+ # Helper to fetch since date
1377
+ def get_count_since(table, date_field, since_dt):
1378
+ try:
1379
+ res = supabase.table(table).select(date_field).gte(date_field, since_dt.isoformat()).execute()
1380
+ return res.data or []
1381
+ except Exception as e:
1382
+ print(f"Error fetching {table}: {e}")
1383
+ return []
1384
+
1385
+ # Fetch data for insights & chart
1386
+ user_dates = get_count_since("telegram_users", "created_at", month_start)
1387
+ image_dates = get_count_since("image_generation_logs", "created_at", month_start)
1388
+ voice_dates = get_count_since("voice_generation_logs", "created_at", month_start)
1389
+
1390
+ # Process Insights
1391
+ def count_periods(data_list, date_key):
1392
+ periods = {"today": 0, "week": 0, "month": len(data_list)}
1393
+ for item in data_list:
1394
+ dt_str = item.get(date_key, "")[:19]
1395
+ try:
1396
+ dt = datetime.fromisoformat(dt_str).replace(tzinfo=timezone.utc)
1397
+ if dt >= today_start: periods["today"] += 1
1398
+ if dt >= week_start: periods["week"] += 1
1399
+ except: continue
1400
+ return periods
1401
+
1402
+ u_insights = count_periods(user_dates, "created_at")
1403
+ combined_gen = image_dates + voice_dates
1404
+ g_insights = count_periods(combined_gen, "created_at")
1405
+
1406
+ # Build 30-day Chart Data
1407
+ chart_labels = []
1408
+ chart_users = []
1409
+ chart_gens = []
1410
+
1411
+ for i in range(29, -1, -1):
1412
+ day = today_start - timedelta(days=i)
1413
+ day_str = day.strftime("%Y-%m-%d")
1414
+ chart_labels.append(day.strftime("%d %b"))
1415
+
1416
+ u_count = sum(1 for x in user_dates if x.get("created_at", "").startswith(day_str))
1417
+ g_count = sum(1 for x in combined_gen if x.get("created_at", "").startswith(day_str))
1418
+
1419
+ chart_users.append(u_count)
1420
+ chart_gens.append(g_count)
1421
+
1422
+ # Pagination for Generations
1423
+ gen_search = request.args.get('gen_search', '').strip()
1424
 
1425
  # 2. Fetch Voice Logs with Pagination
1426
+ v_query = supabase.table("voice_generation_logs").select("*", count="exact").order("created_at", desc=True)
1427
+ if gen_search:
1428
+ v_query = v_query.eq("user_id", gen_search)
1429
+
1430
  v_start = (v_page - 1) * per_page
1431
  v_end = v_start + per_page - 1
1432
+ voices_logs_res = v_query.range(v_start, v_end).execute()
1433
  total_voices_count = voices_logs_res.count or 0
1434
 
1435
  # 3. Fetch Image Logs with Pagination
1436
+ i_query = supabase.table("image_generation_logs").select("*", count="exact").order("created_at", desc=True)
1437
+ if gen_search:
1438
+ i_query = i_query.eq("user_id", gen_search)
1439
+
1440
  i_start = (i_page - 1) * per_page
1441
  i_end = i_start + per_page - 1
1442
  try:
1443
+ image_logs_res = i_query.range(i_start, i_end).execute()
1444
  image_logs_data = image_logs_res.data
1445
  total_images_logs_count = image_logs_res.count or 0
1446
+ except Exception as e:
1447
+ print(f"Image logs fetch error: {e}")
1448
  image_logs_data = []
1449
  total_images_logs_count = 0
1450
 
1451
+ # Global stats totals for dashboard cards
1452
+ stats_query = supabase.table("telegram_users").select("total_images_generated, tier").execute()
1453
+ total_images_gen = sum(u.get('total_images_generated', 0) for u in stats_query.data)
1454
+ premium_count = sum(1 for u in stats_query.data if u.get('tier') == 'paid')
1455
+ total_users_count = len(stats_query.data)
1456
+
1457
  stats = {
1458
  "total_users": total_users_count,
1459
  "total_images": total_images_gen,
1460
  "total_voices": total_voices_count,
1461
+ "premium_users": premium_count,
1462
+ "u_insights": u_insights,
1463
+ "g_insights": g_insights,
1464
+ "chart_data": {
1465
+ "labels": chart_labels,
1466
+ "users": chart_users,
1467
+ "gens": chart_gens
1468
+ }
1469
  }
1470
 
1471
  pages = {
 
1476
  "i_current": i_page,
1477
  "i_total": (total_images_logs_count + per_page - 1) // per_page,
1478
  "search": u_search,
1479
+ "gen_search": gen_search,
1480
  "bot_filter": bot_filter,
1481
  "per_page": per_page
1482
  }
1483
 
1484
+ for user in users_res.data:
1485
+ user['created_at'] = to_wib(user.get('created_at'))
1486
+ user['last_active'] = to_wib(user.get('last_active'))
1487
+
1488
+ for log in voices_logs_res.data:
1489
+ log['created_at'] = to_wib(log.get('created_at'))
1490
+
1491
+ for log in image_logs_data:
1492
+ log['created_at'] = to_wib(log.get('created_at'))
1493
+
1494
  return render_template_string(
1495
  INDEX_TEMPLATE,
1496
  stats=stats,
 
1499
  image_logs=image_logs_data,
1500
  settings=settings,
1501
  pages=pages,
1502
+ now=(datetime.now(timezone.utc) + timedelta(hours=7)).strftime("%H:%M:%S WIB")
1503
  )
1504
  except Exception as e:
1505
  import traceback