Phoe2004 commited on
Commit
bfee34c
ยท
verified ยท
1 Parent(s): 2322f0c

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +38 -11
  2. bot.py +24 -10
  3. index.html +221 -189
  4. start.sh +9 -0
app.py CHANGED
@@ -410,19 +410,46 @@ def get_num_rule(vo_lang='my'):
410
  else:
411
  return NUM_TO_MM_RULE
412
 
413
- GEMINI_MODELS = [
414
- 'gemini-3-flash', # 5 RPM โ€” newest
415
- 'gemini-2.5-flash', # 5 RPM โ€” stable primary
416
- 'gemini-3.1-flash-lite', # 15 RPM โ€” best quota
417
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
- def call_api(msgs, api='Gemini'):
 
 
 
 
420
  if api == 'DeepSeek':
421
  keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
422
  models = ['deepseek-chat']
423
  else:
424
  keys, base = GEMINI_KEYS, 'https://generativelanguage.googleapis.com/v1beta/openai/'
425
- models = GEMINI_MODELS
426
  valid = [(i+1, k) for i, k in enumerate(keys) if k]
427
  if not valid: raise Exception('No API Key available')
428
  if api == 'Gemini':
@@ -437,7 +464,7 @@ def call_api(msgs, api='Gemini'):
437
  r = OpenAI(api_key=k, base_url=base, timeout=120.0).chat.completions.create(
438
  model=mdl, messages=msgs, max_tokens=8192)
439
  if r and r.choices and r.choices[0].message.content:
440
- print(f'โœ… call_api key={n} model={mdl}')
441
  return r.choices[0].message.content.strip(), f'โœ… Key{n} ({mdl})'
442
  except Exception as e:
443
  err = str(e); last_err = e
@@ -445,7 +472,7 @@ def call_api(msgs, api='Gemini'):
445
  if '401' in err or '403' in err: break
446
  if '429' in err: time.sleep(2); break
447
  continue
448
- raise Exception(f'โŒ All keys/models failed: {last_err}')
449
 
450
  def parse_out(text):
451
  sc, ti, ht = '', '', ''
@@ -999,7 +1026,7 @@ def api_draft():
999
  sys_p = sys_p + '\n' + get_num_rule(vo_lang)
1000
  out_txt, key_n = run_stage('ai', call_api,
1001
  [{'role':'system','content':sys_p},
1002
- {'role':'user','content':f'Language:{lang}\n\n{tr}'}], api=api)
1003
  sc, ti, ht = parse_out(out_txt)
1004
 
1005
  rem = -1
@@ -1515,7 +1542,7 @@ def api_process_all():
1515
  {'role':'user','content':f'Language:{src_lang}\n\n{tr}'}]
1516
  out_txt, _ = run_stage('ai', call_api, tid, _prog,
1517
  'โณ AI Script แ€แ€”แ€บแ€ธแ€…แ€ฎแ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€žแ€Šแ€บ', '๐Ÿค– AI Script แ€›แ€ฑแ€ธแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ',
1518
- msgs, api=api_model)
1519
  sc, caption_text, hashtags = parse_out(out_txt)
1520
  _prog(65, '๐Ÿค– AI Script แ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ')
1521
 
@@ -1740,7 +1767,7 @@ def api_admin_payments():
1740
  status = request.args.get('status', 'pending')
1741
  pdb = load_payments_db()
1742
  pays = [p for p in pdb['payments'] if p['status'] == status]
1743
- clean = [dict(p, slip_image='', has_slip=bool(p.get('slip_image'))) for p in pays]
1744
  return jsonify(ok=True, payments=clean)
1745
  except Exception as e:
1746
  return jsonify(ok=False, msg=str(e))
 
410
  else:
411
  return NUM_TO_MM_RULE
412
 
413
+ # โ”€โ”€ Transcript: gemini-3-flash + gemini-2.5-flash (round-robin)
414
+ GEMINI_MODELS_TRANSCRIPT = [
415
+ 'gemini-3-flash', # newest
416
+ 'gemini-2.5-flash', # stable
417
  ]
418
+ # โ”€โ”€ Caption: gemini-3.1-flash-lite + gemini-2.5-flash-lite (round-robin)
419
+ GEMINI_MODELS_CAPTION = [
420
+ 'gemini-3.1-flash-lite', # newest lite
421
+ 'gemini-2.5-flash-lite-preview-06-17', # stable lite
422
+ ]
423
+
424
+ _mdl_tr_idx = 0 # transcript model round-robin index
425
+ _mdl_cap_idx = 0 # caption model round-robin index
426
+ _mdl_lock = threading.Lock()
427
+
428
+ def next_model(purpose='transcript'):
429
+ """Round-robin model selector โ€” spins like a spinner per call."""
430
+ global _mdl_tr_idx, _mdl_cap_idx
431
+ models = GEMINI_MODELS_TRANSCRIPT if purpose == 'transcript' else GEMINI_MODELS_CAPTION
432
+ with _mdl_lock:
433
+ if purpose == 'transcript':
434
+ idx = _mdl_tr_idx % len(models)
435
+ _mdl_tr_idx += 1
436
+ else:
437
+ idx = _mdl_cap_idx % len(models)
438
+ _mdl_cap_idx += 1
439
+ # Return spinner-ordered list starting from current idx
440
+ return models[idx:] + models[:idx]
441
 
442
+ def call_api(msgs, api='Gemini', purpose='transcript'):
443
+ """
444
+ purpose='transcript' โ†’ GEMINI_MODELS_TRANSCRIPT round-robin (gemini-3-flash โ†” gemini-2.5-flash)
445
+ purpose='caption' โ†’ GEMINI_MODELS_CAPTION round-robin (gemini-3.1-flash-lite โ†” gemini-2.5-flash-lite)
446
+ """
447
  if api == 'DeepSeek':
448
  keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
449
  models = ['deepseek-chat']
450
  else:
451
  keys, base = GEMINI_KEYS, 'https://generativelanguage.googleapis.com/v1beta/openai/'
452
+ models = next_model(purpose) # spinner: returns rotation-ordered list
453
  valid = [(i+1, k) for i, k in enumerate(keys) if k]
454
  if not valid: raise Exception('No API Key available')
455
  if api == 'Gemini':
 
464
  r = OpenAI(api_key=k, base_url=base, timeout=120.0).chat.completions.create(
465
  model=mdl, messages=msgs, max_tokens=8192)
466
  if r and r.choices and r.choices[0].message.content:
467
+ print(f'โœ… call_api key={n} model={mdl} purpose={purpose}')
468
  return r.choices[0].message.content.strip(), f'โœ… Key{n} ({mdl})'
469
  except Exception as e:
470
  err = str(e); last_err = e
 
472
  if '401' in err or '403' in err: break
473
  if '429' in err: time.sleep(2); break
474
  continue
475
+ raise Exception(f'โŒ All keys/models failed ({purpose}): {last_err}')
476
 
477
  def parse_out(text):
478
  sc, ti, ht = '', '', ''
 
1026
  sys_p = sys_p + '\n' + get_num_rule(vo_lang)
1027
  out_txt, key_n = run_stage('ai', call_api,
1028
  [{'role':'system','content':sys_p},
1029
+ {'role':'user','content':f'Language:{lang}\n\n{tr}'}], api=api, purpose='transcript')
1030
  sc, ti, ht = parse_out(out_txt)
1031
 
1032
  rem = -1
 
1542
  {'role':'user','content':f'Language:{src_lang}\n\n{tr}'}]
1543
  out_txt, _ = run_stage('ai', call_api, tid, _prog,
1544
  'โณ AI Script แ€แ€”แ€บแ€ธแ€…แ€ฎแ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€žแ€Šแ€บ', '๐Ÿค– AI Script แ€›แ€ฑแ€ธแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ',
1545
+ msgs, api=api_model, purpose='transcript')
1546
  sc, caption_text, hashtags = parse_out(out_txt)
1547
  _prog(65, '๐Ÿค– AI Script แ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ')
1548
 
 
1767
  status = request.args.get('status', 'pending')
1768
  pdb = load_payments_db()
1769
  pays = [p for p in pdb['payments'] if p['status'] == status]
1770
+ clean = [dict(p, slip_image='') for p in pays]
1771
  return jsonify(ok=True, payments=clean)
1772
  except Exception as e:
1773
  return jsonify(ok=False, msg=str(e))
bot.py CHANGED
@@ -619,34 +619,48 @@ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
619
  pay['updated_at'] = _dt.now().isoformat()
620
  save_payments_db(pdb)
621
  add_coins_fn(username, coins_add, 'admin_bot')
622
- new_cap = (q.message.caption or '') + f"\n\nโœ… Approved! +{coins_add} coins โ†’ {username}"
623
- try: await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML)
624
- except: pass
625
- # Notify user
 
 
 
 
 
626
  db = load_db()
627
  tg_id = db['users'].get(username, {}).get('tg_chat_id')
 
628
  if tg_id:
629
  try:
630
  await ctx.bot.send_message(tg_id,
631
- f"โœ… *Coins แ€‘แ€Šแ€ทแ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ!*\n๐Ÿช™ *+{coins_add} Coins* แ€›แ€ฑแ€ฌแ€€แ€บแ€•แ€ผแ€ฎ\n๐Ÿ†” `{payment_id}`",
 
 
 
632
  parse_mode=ParseMode.MARKDOWN)
633
  except: pass
634
  else: # reject
635
  pay['status'] = 'rejected'
636
  pay['updated_at'] = _dt.now().isoformat()
637
  save_payments_db(pdb)
638
- new_cap = (q.message.caption or '') + "\n\nโŒ Rejected"
639
- try: await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML)
640
- except: pass
 
 
 
 
641
  db = load_db()
642
  tg_id = db['users'].get(username, {}).get('tg_chat_id')
643
  if tg_id:
644
  try:
645
  await ctx.bot.send_message(tg_id,
646
- f"โŒ *Payment แ€„แ€ผแ€„แ€บแ€ธแ€•แ€šแ€บแ€แ€ผแ€„แ€บแ€ธแ€แ€ถแ€›แ€žแ€Šแ€บ*\n๐Ÿ†” `{payment_id}`\nแ€™แ€ฑแ€ธแ€™แ€ผแ€”แ€บแ€ธแ€›แ€”แ€บ โ€” @{ADMIN_TG_USERNAME}",
 
 
647
  parse_mode=ParseMode.MARKDOWN)
648
  except: pass
649
- await q.answer()
650
  return
651
 
652
  if data.startswith('cancel|'):
 
619
  pay['updated_at'] = _dt.now().isoformat()
620
  save_payments_db(pdb)
621
  add_coins_fn(username, coins_add, 'admin_bot')
622
+ # Edit original message to show approved status + remove buttons
623
+ new_cap = (q.message.caption or q.message.text or '') + f"\n\nโœ… <b>Approved!</b> +{coins_add} coins โ†’ <code>{username}</code>"
624
+ try:
625
+ await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML, reply_markup=None)
626
+ except:
627
+ try: await q.edit_message_text(text=new_cap, parse_mode=ParseMode.HTML, reply_markup=None)
628
+ except: pass
629
+ await q.answer(f"โœ… Approved +{coins_add} coins โ†’ {username}", show_alert=False)
630
+ # Notify user with new balance
631
  db = load_db()
632
  tg_id = db['users'].get(username, {}).get('tg_chat_id')
633
+ new_bal = db['users'].get(username, {}).get('coins', 0)
634
  if tg_id:
635
  try:
636
  await ctx.bot.send_message(tg_id,
637
+ f"๐ŸŽ‰ *Coins แ€‘แ€Šแ€ทแ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ!*\n"
638
+ f"๐Ÿช™ *+{coins_add} Coins* แ€›แ€ฑแ€ฌแ€€แ€บแ€•แ€ผแ€ฎ\n"
639
+ f"๐Ÿ’ฐ แ€œแ€€แ€บแ€€แ€ปแ€”แ€บ โ€” *{new_bal} Coins*\n"
640
+ f"๐Ÿ†” `{payment_id}`",
641
  parse_mode=ParseMode.MARKDOWN)
642
  except: pass
643
  else: # reject
644
  pay['status'] = 'rejected'
645
  pay['updated_at'] = _dt.now().isoformat()
646
  save_payments_db(pdb)
647
+ new_cap = (q.message.caption or q.message.text or '') + "\n\nโŒ <b>Rejected</b>"
648
+ try:
649
+ await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML, reply_markup=None)
650
+ except:
651
+ try: await q.edit_message_text(text=new_cap, parse_mode=ParseMode.HTML, reply_markup=None)
652
+ except: pass
653
+ await q.answer("โŒ Rejected", show_alert=False)
654
  db = load_db()
655
  tg_id = db['users'].get(username, {}).get('tg_chat_id')
656
  if tg_id:
657
  try:
658
  await ctx.bot.send_message(tg_id,
659
+ f"โŒ *Payment แ€„แ€ผแ€„แ€บแ€ธแ€•แ€šแ€บแ€แ€ผแ€„แ€บแ€ธแ€แ€ถแ€›แ€žแ€Šแ€บ*\n"
660
+ f"๐Ÿ†” `{payment_id}`\n"
661
+ f"แ€™แ€ฑแ€ธแ€™แ€ผแ€”แ€บแ€ธแ€›แ€”แ€บ โ€” @{ADMIN_TG_USERNAME}",
662
  parse_mode=ParseMode.MARKDOWN)
663
  except: pass
 
664
  return
665
 
666
  if data.startswith('cancel|'):
index.html CHANGED
@@ -298,72 +298,189 @@ body{background:var(--bg);color:var(--text);font-family:var(--F);min-height:100v
298
  .modal-close:hover{border-color:var(--red);color:var(--red)}
299
 
300
  /* โ•โ• PAYMENT MODAL โ•โ• */
301
- .pov{display:none;position:fixed;inset:0;z-index:400;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);align-items:flex-end;justify-content:center}
 
 
302
  .pov.on{display:flex}
303
- .psheet{position:relative;width:100%;max-width:500px;background:#fff;border-radius:24px 24px 0 0;
304
- padding:24px 20px 40px;max-height:92vh;overflow-y:auto;
305
- box-shadow:0 -8px 48px rgba(0,0,0,.2);animation:slideUp .28s ease}
306
- @keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
307
- .psheet-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}
308
- .psheet-hdr h2{font-size:1.1rem;font-weight:800;color:var(--text)}
309
- .pclose{width:30px;height:30px;border:1.5px solid var(--border);background:var(--bg);
310
- border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;
311
- font-size:.8rem;color:var(--muted);transition:.2s}
312
- .pclose:hover{border-color:var(--red);color:var(--red)}
313
- /* packages */
314
- .ppkgs{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
315
- .ppkg{border:2px solid var(--border);border-radius:14px;padding:14px 12px;cursor:pointer;
316
- transition:.2s;background:var(--bg);text-align:center;position:relative}
317
- .ppkg:hover{border-color:var(--primary);transform:translateY(-2px);box-shadow:0 4px 14px rgba(91,76,245,.15)}
318
- .ppkg.sel{border-color:var(--primary);background:linear-gradient(135deg,#ede9fe,#ddd6fe)}
319
- .ppkg.pop::before{content:'๐Ÿ”ฅ Popular';position:absolute;top:-10px;left:50%;transform:translateX(-50%);
320
- background:var(--primary);color:#fff;font-size:.62rem;font-weight:700;padding:2px 10px;
321
- border-radius:20px;white-space:nowrap}
322
- .ppkg-c{font-size:1.5rem;font-weight:800;color:var(--primary)}
323
- .ppkg-u{font-size:.75rem;color:var(--muted);margin-bottom:3px}
324
- .ppkg-p{font-size:.88rem;color:var(--orange);font-weight:700}
325
- /* kbz */
326
- .pkbz{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:14px}
327
- .pkbz-t{font-size:.75rem;color:var(--muted);margin-bottom:8px;font-weight:600}
328
- .pkbz-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  .pkbz-row:last-child{margin-bottom:0}
330
- .pkbz-lbl{font-size:.73rem;color:var(--muted)}
331
- .pkbz-val{font-size:.92rem;font-weight:700;color:var(--text)}
332
- .pcopy{background:var(--bg3);border:1px solid var(--border);color:var(--primary);
333
- font-size:.68rem;padding:3px 10px;border-radius:6px;cursor:pointer;font-family:var(--F);font-weight:600;transition:.2s}
334
- .pcopy:hover{background:var(--primary);color:#fff}
335
- /* slip */
336
- .pslip-label{font-size:.78rem;color:var(--muted);margin-bottom:7px;display:block;font-weight:600}
337
- .pslip-drop{border:2px dashed var(--border2);border-radius:12px;padding:22px;text-align:center;
338
- cursor:pointer;transition:.2s;position:relative;overflow:hidden;margin-bottom:14px}
339
- .pslip-drop:hover{border-color:var(--primary);background:rgba(91,76,245,.04)}
 
 
 
 
 
 
 
 
340
  .pslip-drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
341
- .pslip-drop .pi{font-size:1.8rem;margin-bottom:6px}
342
- .pslip-drop .pt{font-size:.78rem;color:var(--muted)}
343
- .pslip-preview{width:100%;max-height:160px;object-fit:contain;border-radius:8px;display:none;margin-top:8px}
344
- .psubmit{width:100%;padding:14px;border:none;border-radius:12px;
345
- background:linear-gradient(135deg,var(--primary),var(--purple));
346
- color:#fff;font-family:var(--F);font-size:.92rem;font-weight:700;
347
- cursor:pointer;transition:.2s;display:none}
 
 
 
 
 
 
 
 
348
  .psubmit.on{display:block}
349
- .psubmit:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 20px rgba(91,76,245,.4)}
350
- .psubmit:disabled{opacity:.5;cursor:not-allowed}
351
- .pinfo{font-size:.78rem;color:var(--muted);margin-bottom:4px;text-align:center;min-height:20px}
352
- /* history */
353
- .ph-card{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:10px}
 
 
 
 
 
354
  .ph-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
355
- .ph-coins{font-size:1.1rem;font-weight:800;color:var(--primary)}
356
- .ph-badge{font-size:.7rem;padding:3px 10px;border-radius:20px;font-weight:700}
357
- .ph-badge.pending{background:#fff7ed;color:#92400e;border:1px solid #fcd34d}
358
- .ph-badge.approved{background:#d1fae5;color:#065f46;border:1px solid #6ee7b7}
359
- .ph-badge.rejected{background:#fee2e2;color:#991b1b;border:1px solid #fca5a5}
360
- .ph-meta{font-size:.72rem;color:var(--muted)}
361
- .ph-empty{text-align:center;padding:32px 0;color:var(--muted);font-size:.85rem}
362
- .tab2{display:flex;gap:6px;margin-bottom:16px}
363
- .tab2 button{flex:1;padding:9px;border:1.5px solid var(--border);background:#fff;
364
- color:var(--muted);font-family:var(--F);font-size:.8rem;font-weight:600;
365
- border-radius:9px;cursor:pointer;transition:.2s}
366
- .tab2 button.on{border-color:var(--primary);background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:var(--primary)}
367
  </style>
368
  </head>
369
  <body>
@@ -600,33 +717,8 @@ body{background:var(--bg);color:var(--text);font-family:var(--F);min-height:100v
600
  <div id="admmsg" class="admmsg"></div>
601
  <button style="padding:8px 14px;background:var(--bg);border:1.5px solid var(--border);border-radius:8px;color:var(--muted);font-family:var(--F);font-size:.75rem;font-weight:600;cursor:pointer;margin-top:4px;transition:.2s" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'" onclick="loadUsers()"><i class="fas fa-users"></i> Load Users</button>
602
  <div id="utw" style="overflow-x:auto"></div>
603
-
604
- <!-- โ•โ• PENDING PAYMENTS โ•โ• -->
605
- <div style="margin-top:18px;border-top:1.5px solid var(--border);padding-top:14px">
606
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
607
- <div style="font-weight:700;font-size:.85rem;color:var(--text);display:flex;align-items:center;gap:7px">
608
- <span style="background:#fff7ed;color:#92400e;border:1px solid #fcd34d;border-radius:6px;padding:3px 8px;font-size:.72rem;font-weight:700">โณ Pending</span>
609
- Payments
610
- <span id="adm-pay-count" style="background:var(--red);color:#fff;border-radius:20px;padding:1px 8px;font-size:.7rem;display:none">0</span>
611
- </div>
612
- <button onclick="loadAdminPays()" style="padding:5px 11px;background:var(--primary);color:#fff;border:none;border-radius:7px;font-family:var(--F);font-size:.73rem;font-weight:600;cursor:pointer;transition:.2s">๐Ÿ”„ Refresh</button>
613
- </div>
614
- <div id="adm-pay-list" style="display:flex;flex-direction:column;gap:9px">
615
- <div style="text-align:center;font-size:.78rem;color:var(--muted);padding:14px">
616
- Refresh แ€”แ€พแ€ญแ€•แ€บแ€•แ€ผแ€ฎแ€ธ Pending Payments แ€€แ€ผแ€Šแ€ทแ€บแ€•แ€ซ
617
- </div>
618
- </div>
619
- </div>
620
  </div>
621
 
622
- <!-- SLIP LIGHTBOX -->
623
- <div id="slip-lb" onclick="closeSlipLb()" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);backdrop-filter:blur(6px);align-items:center;justify-content:center;flex-direction:column;gap:12px">
624
- <button onclick="closeSlipLb()" style="position:absolute;top:18px;right:18px;background:rgba(255,255,255,.15);border:none;border-radius:50%;width:38px;height:38px;color:#fff;font-size:1.1rem;cursor:pointer">โœ•</button>
625
- <img id="slip-lb-img" src="" style="max-width:90vw;max-height:78vh;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.5);object-fit:contain">
626
- <div id="slip-lb-info" style="color:rgba(255,255,255,.8);font-size:.82rem;text-align:center"></div>
627
- <div style="display:flex;gap:10px" id="slip-lb-btns"></div>
628
- </div>
629
-
630
  </div>
631
  </div>
632
 
@@ -761,7 +853,7 @@ function eApp(u,coins,isAdm,displayName){
761
  document.getElementById('dav').textContent=dn[0].toUpperCase();
762
  document.getElementById('dcoins').textContent=isAdm?'โˆž':coins;
763
  fetch('/api/config').then(r=>r.json()).then(d=>{if(d.admin_tg)window._ADMIN_TG=d.admin_tg;}).catch(()=>{});
764
- if(isAdm){document.getElementById('admp').style.display='block';document.getElementById('adm-dlink').style.display='flex';document.getElementById('cpill').style.display='none';document.getElementById('dcoins').textContent='โˆž';loadAdminPays();}
765
  renderV();
766
  window.addEventListener('resize',()=>{const v=document.getElementById('pvid');if(v.src&&v.videoWidth)syncCanvasInner(v);});
767
  fetch('/api/gemini_voices').then(r=>r.ok?r.json():null).then(d=>{if(d?.ok){GV=d.voices;if(document.getElementById('engine').value==='gemini')renderV();}}).catch(()=>{});
@@ -805,8 +897,12 @@ async function initPayModal(){
805
  }
806
 
807
  function renderPkgs(){
 
 
808
  document.getElementById('ppkgs').innerHTML=_pkgs.map((p,i)=>`
809
  <div class="ppkg${i===1?' pop':''}" id="ppkg${i}" onclick="selPkg(${i})">
 
 
810
  <div class="ppkg-c">${p.coins}</div>
811
  <div class="ppkg-u">Coins</div>
812
  <div class="ppkg-p">${p.price}</div>
@@ -818,6 +914,8 @@ function selPkg(i){
818
  document.getElementById('ppkg'+i).classList.add('sel');
819
  _selPkg=_pkgs[i];
820
  document.getElementById('pinfo').textContent=`โœ… ${_selPkg.coins} Coins โ€” ${_selPkg.price} แ€›แ€ฝแ€ฑแ€ธแ€แ€ปแ€šแ€บแ€‘แ€ฌแ€ธแ€žแ€Šแ€บ`;
 
 
821
  checkPReady();
822
  }
823
 
@@ -882,7 +980,7 @@ async function loadPayHistory(){
882
  <span class="ph-badge ${p.status}">${lbl[p.status]||p.status}</span>
883
  </div>
884
  <div class="ph-meta">๐Ÿ†” ${p.id} &nbsp;ยท&nbsp; ${p.created_at.slice(0,16).replace('T',' ')}</div>
885
- ${p.admin_note?`<div style="margin-top:5px;font-size:.75rem;color:var(--muted)">๐Ÿ“ ${p.admin_note}</div>`:''}
886
  </div>`).join('');
887
  }catch(e){ el.innerHTML=`<div class="ph-empty">โŒ ${e.message}</div>`; }
888
  }
@@ -1176,94 +1274,6 @@ async function admDel(){const u=document.getElementById('au-del').value.trim();i
1176
  async function loadUsers(){const r=await fetch('/api/admin/users?caller='+encodeURIComponent(U));if(!r.ok)return;const d=await r.json();if(!d.ok)return;const w=document.getElementById('utw');if(!d.users.length){w.innerHTML='<div style="font-size:.75rem;color:var(--muted);padding:6px">No users</div>';return;}let h='<table class="ut"><thead><tr><th>Username</th><th>Coins</th><th>Videos</th><th>Created</th><th></th></tr></thead><tbody>';d.users.forEach(u=>{h+=`<tr><td>${u.username}</td><td>๐Ÿช™${u.coins}</td><td>${u.videos}</td><td>${u.created||''}</td><td><button class="delbtn" onclick="qDel('${u.username}')"><i class="fas fa-trash"></i></button></td></tr>`;});w.innerHTML=h+'</tbody></table>';}
1177
  async function qDel(u){if(!confirm('Delete '+u+'?'))return;const r=await fetch('/api/admin/delete_user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,username:u})});const d=await r.json();toast(d.msg);if(d.ok)loadUsers();}
1178
 
1179
- /* โ•โ• ADMIN PENDING PAYMENTS โ•โ• */
1180
- async function loadAdminPays(){
1181
- const el=document.getElementById('adm-pay-list');
1182
- el.innerHTML='<div style="text-align:center;font-size:.78rem;color:var(--muted);padding:14px">โณ Loadingโ€ฆ</div>';
1183
- try{
1184
- const r=await fetch('/api/admin/payments?caller='+encodeURIComponent(U)+'&status=pending');
1185
- const d=await r.json();
1186
- if(!d.ok){el.innerHTML='<div style="text-align:center;font-size:.78rem;color:var(--red);padding:14px">โŒ '+(d.msg||'Error')+'</div>';return;}
1187
- const pays=d.payments||[];
1188
- const cnt=document.getElementById('adm-pay-count');
1189
- if(pays.length){cnt.textContent=pays.length;cnt.style.display='inline';}
1190
- else{cnt.style.display='none';}
1191
- if(!pays.length){el.innerHTML='<div style="text-align:center;font-size:.78rem;color:var(--muted);padding:14px">โœ… Pending payment แ€™แ€›แ€พแ€ญแ€•แ€ซ</div>';return;}
1192
- el.innerHTML=pays.map(p=>`
1193
- <div id="apc-${p.id}" style="background:var(--bg);border:1.5px solid #fcd34d;border-radius:12px;padding:12px;position:relative">
1194
- <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;flex-wrap:wrap">
1195
- <div>
1196
- <div style="font-weight:700;font-size:.85rem;color:var(--text)">๐Ÿ‘ค ${p.username}</div>
1197
- <div style="font-size:.75rem;color:var(--muted);margin-top:2px">๐Ÿช™ ${p.coins} Coins &nbsp;ยท&nbsp; ${p.price} &nbsp;ยท&nbsp; ๐Ÿ†” <code style="font-size:.7rem">${p.id}</code></div>
1198
- <div style="font-size:.7rem;color:var(--muted2);margin-top:2px">${(p.created_at||'').slice(0,16).replace('T',' ')}</div>
1199
- </div>
1200
- <span style="background:#fff7ed;color:#92400e;border:1px solid #fcd34d;border-radius:6px;padding:2px 8px;font-size:.7rem;font-weight:700;white-space:nowrap">โณ Pending</span>
1201
- </div>
1202
- <div style="display:flex;gap:7px;margin-top:10px;flex-wrap:wrap">
1203
- ${p.has_slip!==false?`<button onclick="viewSlip('${p.id}','${p.username}',${p.coins},'${p.price}')" style="padding:6px 12px;background:var(--bg2);border:1.5px solid var(--border2);border-radius:8px;font-family:var(--F);font-size:.75rem;font-weight:600;cursor:pointer;color:var(--text);transition:.2s" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border2)'">๐Ÿ–ผ Slip แ€€แ€ผแ€Šแ€ทแ€บ</button>`:'<span style="font-size:.72rem;color:var(--muted);padding:6px 0">๐Ÿ“Ž Slip แ€™แ€•แ€ซ</span>'}
1204
- <button onclick="admApprove('${p.id}','${p.username}',${p.coins})" style="padding:6px 14px;background:var(--green);color:#fff;border:none;border-radius:8px;font-family:var(--F);font-size:.75rem;font-weight:600;cursor:pointer;transition:.2s">โœ… Approve +${p.coins}</button>
1205
- <button onclick="admReject('${p.id}','${p.username}')" style="padding:6px 14px;background:var(--red);color:#fff;border:none;border-radius:8px;font-family:var(--F);font-size:.75rem;font-weight:600;cursor:pointer;transition:.2s">โŒ Reject</button>
1206
- </div>
1207
- </div>`).join('');
1208
- }catch(e){el.innerHTML=`<div style="text-align:center;font-size:.78rem;color:var(--red);padding:14px">โŒ ${e.message}</div>`;}
1209
- }
1210
-
1211
- async function admApprove(pid,uname,coins){
1212
- if(!confirm(`โœ… ${uname} แ€€แ€ญแ€ฏ +${coins} Coins Approve แ€™แ€œแ€ฏแ€•แ€บแ€†แ€ฑแ€ฌแ€„แ€บแ€›แ€”แ€บ แ€žแ€ฑแ€แ€ปแ€ฌแ€•แ€ซแ€žแ€œแ€ฌแ€ธ?`)) return;
1213
- try{
1214
- const r=await fetch('/api/admin/payment/approve',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,payment_id:pid})});
1215
- const d=await r.json();
1216
- if(d.ok){
1217
- toast('โœ… '+d.msg);
1218
- const card=document.getElementById('apc-'+pid);
1219
- if(card){card.style.opacity='0.4';card.style.pointerEvents='none';setTimeout(()=>{card.remove();},1500);}
1220
- const cnt=document.getElementById('adm-pay-count');
1221
- const cur=parseInt(cnt.textContent)||0;
1222
- if(cur-1<=0){cnt.style.display='none';}else{cnt.textContent=cur-1;}
1223
- } else { toast('โŒ '+(d.msg||'Failed')); }
1224
- }catch(e){toast('โŒ '+e.message);}
1225
- }
1226
-
1227
- async function admReject(pid,uname){
1228
- const note=prompt(`โŒ ${uname} แ Payment Reject แ€›แ€”แ€บ โ€” แ€กแ€€แ€ผแ€ฑแ€ฌแ€„แ€บแ€ธแ€•แ€ผแ€แ€ปแ€€แ€บ (Optional):`)||'';
1229
- try{
1230
- const r=await fetch('/api/admin/payment/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,payment_id:pid,note})});
1231
- const d=await r.json();
1232
- if(d.ok){
1233
- toast('โŒ Rejected โ€” '+uname);
1234
- const card=document.getElementById('apc-'+pid);
1235
- if(card){card.style.opacity='0.4';card.style.pointerEvents='none';setTimeout(()=>{card.remove();},1500);}
1236
- } else { toast('โŒ '+(d.msg||'Failed')); }
1237
- }catch(e){toast('โŒ '+e.message);}
1238
- }
1239
-
1240
- async function viewSlip(pid,uname,coins,price){
1241
- const lb=document.getElementById('slip-lb');
1242
- lb.style.display='flex';
1243
- document.getElementById('slip-lb-img').src='';
1244
- document.getElementById('slip-lb-info').textContent='โณ Loading slipโ€ฆ';
1245
- document.getElementById('slip-lb-btns').innerHTML='';
1246
- try{
1247
- const r=await fetch(`/api/admin/payment/slip/${pid}?caller=${encodeURIComponent(U)}`);
1248
- const d=await r.json();
1249
- if(d.ok&&d.slip_image){
1250
- document.getElementById('slip-lb-img').src=d.slip_image;
1251
- document.getElementById('slip-lb-info').textContent=`๐Ÿ‘ค ${uname} ยท ๐Ÿช™ ${coins} Coins ยท ${price} ยท ๐Ÿ†” ${pid}`;
1252
- document.getElementById('slip-lb-btns').innerHTML=`
1253
- <button onclick="closeSlipLb();admApprove('${pid}','${uname}',${coins})" style="padding:9px 20px;background:var(--green);color:#fff;border:none;border-radius:9px;font-family:var(--F);font-size:.8rem;font-weight:700;cursor:pointer">โœ… Approve +${coins}</button>
1254
- <button onclick="closeSlipLb();admReject('${pid}','${uname}')" style="padding:9px 20px;background:var(--red);color:#fff;border:none;border-radius:9px;font-family:var(--F);font-size:.8rem;font-weight:700;cursor:pointer">โŒ Reject</button>`;
1255
- } else {
1256
- document.getElementById('slip-lb-info').textContent='๐Ÿ“Ž Slip แ€•แ€ฏแ€ถ แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ';
1257
- }
1258
- }catch(e){document.getElementById('slip-lb-info').textContent='โŒ '+e.message;}
1259
- }
1260
-
1261
- function closeSlipLb(){
1262
- const lb=document.getElementById('slip-lb');
1263
- lb.style.display='none';
1264
- document.getElementById('slip-lb-img').src='';
1265
- }
1266
-
1267
  /* โ•โ• UTILS โ•โ• */
1268
  function toast(m){const e=document.getElementById('toast');e.textContent=m;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),2800);}
1269
 
@@ -1289,10 +1299,14 @@ document.addEventListener('keydown',e=>{if(e.key==='Enter'&&document.getElementB
1289
  <!-- โ•โ• PAYMENT MODAL โ•โ• -->
1290
  <div class="pov" id="pov" onclick="if(event.target===this)closePay()">
1291
  <div class="psheet">
 
 
1292
  <div class="psheet-hdr">
1293
- <h2 id="p-title">๐Ÿช™ Coins แ€แ€šแ€บแ€›แ€”แ€บ</h2>
1294
  <button class="pclose" onclick="closePay()"><i class="fas fa-times"></i></button>
1295
  </div>
 
 
1296
  <div class="tab2">
1297
  <button id="ptab-buy" class="on" onclick="showPayTab('buy')">๐Ÿ›’ แ€แ€šแ€บแ€›แ€”แ€บ</button>
1298
  <button id="ptab-his" onclick="showPayTab('his')">๐Ÿ“‹ แ€™แ€พแ€แ€บแ€แ€™แ€บแ€ธ</button>
@@ -1300,56 +1314,74 @@ document.addEventListener('keydown',e=>{if(e.key==='Enter'&&document.getElementB
1300
 
1301
  <!-- BUY TAB -->
1302
  <div id="ptab-buy-content">
 
 
1303
  <div class="ppkgs" id="ppkgs"></div>
 
 
1304
  <div class="pinfo" id="pinfo">Package แ€แ€…แ€บแ€แ€ฏ แ€›แ€ฝแ€ฑแ€ธแ€แ€ปแ€šแ€บแ€•แ€ซ</div>
 
 
1305
  <div class="pkbz">
1306
  <div class="pkbz-t">๐Ÿ“ฑ KBZ Pay / Wave / AYA Pay</div>
1307
  <div class="pkbz-row">
1308
  <span class="pkbz-lbl">Name</span>
1309
- <div style="display:flex;align-items:center;gap:7px">
1310
  <span class="pkbz-val" id="p-kbz-name">โ€”</span>
1311
- <button class="pcopy" onclick="pcopy('p-kbz-name')">Copy</button>
1312
  </div>
1313
  </div>
1314
  <div class="pkbz-row">
1315
  <span class="pkbz-lbl">Phone</span>
1316
- <div style="display:flex;align-items:center;gap:7px">
1317
  <span class="pkbz-val" id="p-kbz-num">โ€”</span>
1318
- <button class="pcopy" onclick="pcopy('p-kbz-num')">Copy</button>
1319
  </div>
1320
  </div>
1321
  </div>
1322
- <div class="pkbz" style="margin-bottom:14px">
 
 
1323
  <div class="pkbz-t">๐Ÿฆ SCB Bank</div>
1324
  <div class="pkbz-row">
1325
  <span class="pkbz-lbl">Name</span>
1326
- <div style="display:flex;align-items:center;gap:7px">
1327
  <span class="pkbz-val" id="p-scb-name">โ€”</span>
1328
- <button class="pcopy" onclick="pcopy('p-scb-name')">Copy</button>
1329
  </div>
1330
  </div>
1331
  <div class="pkbz-row">
1332
  <span class="pkbz-lbl">Account</span>
1333
- <div style="display:flex;align-items:center;gap:7px">
1334
  <span class="pkbz-val" id="p-scb-num">โ€”</span>
1335
- <button class="pcopy" onclick="pcopy('p-scb-num')">Copy</button>
1336
  </div>
1337
  </div>
1338
  </div>
 
 
1339
  <span class="pslip-label">๐Ÿ“ธ แ€„แ€ฝแ€ฑแ€œแ€ฝแ€พแ€ฒ Slip แ€•แ€ฏแ€ถ แ€แ€„แ€บแ€•แ€ฑแ€ธแ€•แ€ซ</span>
1340
  <div class="pslip-drop" id="pslip-drop">
1341
  <input type="file" accept="image/*" id="pslip-input" onchange="handlePSlip(this)">
1342
  <div class="pi" id="pslip-icon">๐Ÿ“Ž</div>
1343
- <div class="pt" id="pslip-txt">Slip แ€•แ€ฏแ€ถ แ€‘แ€ญแ€ฏแ€”แ€ฑแ€›แ€ฌแ€‘แ€Šแ€ทแ€บแ€•แ€ซ แ€žแ€ญแ€ฏแ€ทแ€™แ€Ÿแ€ฏแ€แ€บ แ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ</div>
1344
  <img id="pslip-prev" class="pslip-preview">
1345
  </div>
1346
- <button class="psubmit" id="psubmit" onclick="submitPay()" disabled>๐Ÿ’ฐ Payment แ€แ€„แ€บแ€•แ€ฑแ€ธแ€•แ€ซ</button>
 
 
 
 
 
1347
  </div>
1348
 
1349
  <!-- HISTORY TAB -->
1350
  <div id="ptab-his-content" style="display:none">
1351
- <div id="ph-list"><div class="ph-empty">โณ Loadingโ€ฆ</div></div>
 
 
1352
  </div>
 
1353
  </div>
1354
  </div>
1355
 
 
298
  .modal-close:hover{border-color:var(--red);color:var(--red)}
299
 
300
  /* โ•โ• PAYMENT MODAL โ•โ• */
301
+ .pov{display:none;position:fixed;inset:0;z-index:400;
302
+ background:rgba(8,8,20,.75);backdrop-filter:blur(12px);
303
+ align-items:flex-end;justify-content:center}
304
  .pov.on{display:flex}
305
+
306
+ @keyframes sheetUp{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}
307
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
308
+ @keyframes shimmer{0%{background-position:200% center}100%{background-position:-200% center}}
309
+ @keyframes pulse-glow{0%,100%{box-shadow:0 0 0 0 rgba(139,92,246,.3)}50%{box-shadow:0 0 0 8px rgba(139,92,246,0)}}
310
+
311
+ .psheet{
312
+ position:relative;width:100%;max-width:520px;
313
+ background:linear-gradient(165deg,#0f0f1e 0%,#13112b 60%,#0e1628 100%);
314
+ border-radius:28px 28px 0 0;
315
+ padding:0 0 44px;max-height:93vh;overflow-y:auto;
316
+ box-shadow:0 -4px 60px rgba(91,76,245,.25),0 -1px 0 rgba(255,255,255,.06);
317
+ animation:sheetUp .32s cubic-bezier(.34,1.1,.64,1)}
318
+ .psheet::-webkit-scrollbar{width:3px}
319
+ .psheet::-webkit-scrollbar-track{background:transparent}
320
+ .psheet::-webkit-scrollbar-thumb{background:rgba(139,92,246,.3);border-radius:10px}
321
+
322
+ /* header */
323
+ .psheet-hdr{
324
+ padding:20px 22px 0;
325
+ display:flex;align-items:center;justify-content:space-between;
326
+ position:sticky;top:0;z-index:2;
327
+ background:linear-gradient(165deg,#0f0f1e,#13112b);
328
+ padding-bottom:16px;
329
+ border-bottom:1px solid rgba(255,255,255,.06)}
330
+ .psheet-hdr h2{font-size:1.05rem;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px}
331
+ .psheet-hdr h2 span{
332
+ display:inline-flex;align-items:center;justify-content:center;
333
+ width:32px;height:32px;background:linear-gradient(135deg,#7c3aed,#5b4cf5);
334
+ border-radius:10px;font-size:.9rem}
335
+ .pclose{
336
+ width:32px;height:32px;border:1px solid rgba(255,255,255,.12);
337
+ background:rgba(255,255,255,.06);border-radius:9px;cursor:pointer;
338
+ display:flex;align-items:center;justify-content:center;
339
+ font-size:.8rem;color:rgba(255,255,255,.5);transition:.2s}
340
+ .pclose:hover{background:rgba(239,68,68,.15);border-color:rgba(239,68,68,.4);color:#f87171}
341
+
342
+ /* tab2 */
343
+ .tab2{display:flex;gap:6px;margin:18px 22px 0}
344
+ .tab2 button{
345
+ flex:1;padding:10px;border:1px solid rgba(255,255,255,.08);
346
+ background:rgba(255,255,255,.04);
347
+ color:rgba(255,255,255,.4);font-family:var(--F);font-size:.8rem;font-weight:600;
348
+ border-radius:10px;cursor:pointer;transition:.2s}
349
+ .tab2 button.on{
350
+ border-color:rgba(139,92,246,.5);
351
+ background:linear-gradient(135deg,rgba(124,58,237,.2),rgba(91,76,245,.15));
352
+ color:#c4b5fd}
353
+
354
+ /* package grid */
355
+ .ppkgs{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:18px 22px 0}
356
+
357
+ .ppkg{
358
+ border:1.5px solid rgba(255,255,255,.07);
359
+ border-radius:18px;padding:18px 14px 14px;cursor:pointer;
360
+ transition:all .22s cubic-bezier(.34,1.1,.64,1);
361
+ background:rgba(255,255,255,.04);
362
+ text-align:center;position:relative;overflow:hidden}
363
+ .ppkg::before{
364
+ content:'';position:absolute;inset:0;
365
+ background:radial-gradient(ellipse at 50% 0%,rgba(139,92,246,.12),transparent 70%);
366
+ opacity:0;transition:.2s}
367
+ .ppkg:hover{border-color:rgba(139,92,246,.4);transform:translateY(-3px);
368
+ box-shadow:0 8px 24px rgba(91,76,245,.2);background:rgba(139,92,246,.08)}
369
+ .ppkg:hover::before{opacity:1}
370
+ .ppkg.sel{
371
+ border-color:rgba(167,139,250,.6);
372
+ background:linear-gradient(145deg,rgba(124,58,237,.18),rgba(91,76,245,.12));
373
+ box-shadow:0 0 0 3px rgba(139,92,246,.15),0 8px 28px rgba(91,76,245,.25)}
374
+ .ppkg.sel::before{opacity:1}
375
+
376
+ .ppkg.pop{border-color:rgba(245,158,11,.35);background:rgba(245,158,11,.05)}
377
+ .ppkg.pop::after{
378
+ content:'๐Ÿ”ฅ Popular';position:absolute;top:-1px;left:50%;transform:translateX(-50%);
379
+ background:linear-gradient(90deg,#f59e0b,#fbbf24);color:#1a0a00;
380
+ font-size:.58rem;font-weight:800;padding:3px 10px;border-radius:0 0 10px 10px;
381
+ white-space:nowrap;letter-spacing:.03em}
382
+ .ppkg.pop:hover{border-color:rgba(245,158,11,.6);box-shadow:0 8px 24px rgba(245,158,11,.2)}
383
+ .ppkg.pop.sel{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.15),0 8px 28px rgba(245,158,11,.2)}
384
+
385
+ .ppkg-icon{font-size:1.6rem;margin-bottom:6px;display:block;
386
+ filter:drop-shadow(0 2px 8px rgba(0,0,0,.3))}
387
+ .ppkg-c{
388
+ font-size:1.8rem;font-weight:800;
389
+ background:linear-gradient(135deg,#c4b5fd,#a78bfa);
390
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
391
+ line-height:1;margin-bottom:2px}
392
+ .ppkg.pop .ppkg-c{
393
+ background:linear-gradient(135deg,#fde68a,#fbbf24);
394
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
395
+ .ppkg-u{font-size:.65rem;color:rgba(255,255,255,.4);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px}
396
+ .ppkg-p{
397
+ font-size:.78rem;font-weight:700;
398
+ background:rgba(255,255,255,.08);border-radius:6px;
399
+ padding:4px 8px;color:rgba(255,255,255,.7);display:inline-block}
400
+ .ppkg.pop .ppkg-p{background:rgba(245,158,11,.15);color:#fbbf24}
401
+
402
+ /* selected badge */
403
+ .ppkg.sel .ppkg-c{background:linear-gradient(135deg,#e9d5ff,#c4b5fd);
404
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
405
+
406
+ /* pinfo */
407
+ .pinfo{
408
+ margin:14px 22px 0;padding:10px 14px;
409
+ background:rgba(139,92,246,.1);border:1px solid rgba(139,92,246,.2);
410
+ border-radius:10px;font-size:.78rem;color:#c4b5fd;
411
+ text-align:center;min-height:36px;display:flex;align-items:center;justify-content:center;
412
+ font-weight:600}
413
+
414
+ /* payment info box */
415
+ .pkbz{
416
+ margin:14px 22px 0;
417
+ background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);
418
+ border-radius:14px;padding:14px;overflow:hidden;position:relative}
419
+ .pkbz::before{
420
+ content:'';position:absolute;top:0;left:0;right:0;height:1px;
421
+ background:linear-gradient(90deg,transparent,rgba(139,92,246,.4),transparent)}
422
+ .pkbz-t{
423
+ font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
424
+ color:rgba(255,255,255,.35);margin-bottom:10px;display:flex;align-items:center;gap:6px}
425
+ .pkbz-t::after{content:'';flex:1;height:1px;background:rgba(255,255,255,.06)}
426
+ .pkbz-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
427
  .pkbz-row:last-child{margin-bottom:0}
428
+ .pkbz-lbl{font-size:.7rem;color:rgba(255,255,255,.35);font-weight:500}
429
+ .pkbz-val{font-size:.88rem;font-weight:700;color:rgba(255,255,255,.9)}
430
+ .pcopy{
431
+ background:rgba(139,92,246,.12);border:1px solid rgba(139,92,246,.25);
432
+ color:#a78bfa;font-size:.65rem;padding:4px 10px;border-radius:6px;
433
+ cursor:pointer;font-family:var(--F);font-weight:700;transition:.2s}
434
+ .pcopy:hover{background:rgba(139,92,246,.25);border-color:rgba(167,139,250,.5);color:#c4b5fd}
435
+
436
+ /* slip upload */
437
+ .pslip-label{
438
+ font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
439
+ color:rgba(255,255,255,.4);margin:14px 22px 8px;display:block}
440
+ .pslip-drop{
441
+ margin:0 22px;border:1.5px dashed rgba(139,92,246,.3);border-radius:14px;
442
+ padding:24px 20px;text-align:center;cursor:pointer;transition:.25s;
443
+ position:relative;overflow:hidden;
444
+ background:rgba(139,92,246,.04)}
445
+ .pslip-drop:hover{border-color:rgba(167,139,250,.6);background:rgba(139,92,246,.09)}
446
  .pslip-drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
447
+ .pslip-drop .pi{font-size:2rem;margin-bottom:8px;display:block;
448
+ filter:drop-shadow(0 2px 8px rgba(139,92,246,.4))}
449
+ .pslip-drop .pt{font-size:.75rem;color:rgba(255,255,255,.35);line-height:1.5}
450
+ .pslip-preview{width:100%;max-height:160px;object-fit:contain;border-radius:10px;display:none;margin-top:10px;
451
+ box-shadow:0 4px 20px rgba(0,0,0,.4)}
452
+
453
+ /* submit button */
454
+ .psubmit{
455
+ display:none;width:calc(100% - 44px);margin:16px 22px 0;
456
+ padding:15px;border:none;border-radius:14px;
457
+ background:linear-gradient(135deg,#7c3aed,#5b4cf5,#4f46e5);
458
+ background-size:200% auto;
459
+ color:#fff;font-family:var(--F);font-size:.92rem;font-weight:800;
460
+ cursor:pointer;transition:.3s;letter-spacing:.01em;
461
+ box-shadow:0 4px 24px rgba(91,76,245,.35)}
462
  .psubmit.on{display:block}
463
+ .psubmit:hover:not(:disabled){
464
+ background-position:right center;
465
+ transform:translateY(-2px);box-shadow:0 8px 32px rgba(91,76,245,.5)}
466
+ .psubmit:disabled{opacity:.4;cursor:not-allowed;transform:none}
467
+
468
+ /* history tab */
469
+ .ph-card{
470
+ background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
471
+ border-radius:14px;padding:14px 16px;margin-bottom:9px;
472
+ animation:fadeIn .2s ease both}
473
  .ph-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
474
+ .ph-coins{font-size:1.05rem;font-weight:800;
475
+ background:linear-gradient(135deg,#c4b5fd,#a78bfa);
476
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
477
+ .ph-badge{font-size:.65rem;padding:4px 10px;border-radius:20px;font-weight:700;letter-spacing:.03em}
478
+ .ph-badge.pending{background:rgba(251,191,36,.12);color:#fbbf24;border:1px solid rgba(251,191,36,.25)}
479
+ .ph-badge.approved{background:rgba(52,211,153,.12);color:#34d399;border:1px solid rgba(52,211,153,.25)}
480
+ .ph-badge.rejected{background:rgba(239,68,68,.12);color:#f87171;border:1px solid rgba(239,68,68,.25)}
481
+ .ph-meta{font-size:.69rem;color:rgba(255,255,255,.3)}
482
+ .ph-empty{text-align:center;padding:36px 0;color:rgba(255,255,255,.25);font-size:.85rem}
483
+ .ph-list-wrap{padding:0 22px}
 
 
484
  </style>
485
  </head>
486
  <body>
 
717
  <div id="admmsg" class="admmsg"></div>
718
  <button style="padding:8px 14px;background:var(--bg);border:1.5px solid var(--border);border-radius:8px;color:var(--muted);font-family:var(--F);font-size:.75rem;font-weight:600;cursor:pointer;margin-top:4px;transition:.2s" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'" onclick="loadUsers()"><i class="fas fa-users"></i> Load Users</button>
719
  <div id="utw" style="overflow-x:auto"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  </div>
721
 
 
 
 
 
 
 
 
 
722
  </div>
723
  </div>
724
 
 
853
  document.getElementById('dav').textContent=dn[0].toUpperCase();
854
  document.getElementById('dcoins').textContent=isAdm?'โˆž':coins;
855
  fetch('/api/config').then(r=>r.json()).then(d=>{if(d.admin_tg)window._ADMIN_TG=d.admin_tg;}).catch(()=>{});
856
+ if(isAdm){document.getElementById('admp').style.display='block';document.getElementById('adm-dlink').style.display='flex';document.getElementById('cpill').style.display='none';document.getElementById('dcoins').textContent='โˆž';}
857
  renderV();
858
  window.addEventListener('resize',()=>{const v=document.getElementById('pvid');if(v.src&&v.videoWidth)syncCanvasInner(v);});
859
  fetch('/api/gemini_voices').then(r=>r.ok?r.json():null).then(d=>{if(d?.ok){GV=d.voices;if(document.getElementById('engine').value==='gemini')renderV();}}).catch(()=>{});
 
897
  }
898
 
899
  function renderPkgs(){
900
+ const icons=['๐Ÿฅ‰','๐Ÿฅˆ','๐Ÿฅ‡','๐Ÿ’Ž'];
901
+ const tiers=['Starter','Basic','Pro','Unlimited'];
902
  document.getElementById('ppkgs').innerHTML=_pkgs.map((p,i)=>`
903
  <div class="ppkg${i===1?' pop':''}" id="ppkg${i}" onclick="selPkg(${i})">
904
+ <span class="ppkg-icon">${icons[i]||'๐Ÿช™'}</span>
905
+ <div style="font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,.3);margin-bottom:4px">${tiers[i]||''}</div>
906
  <div class="ppkg-c">${p.coins}</div>
907
  <div class="ppkg-u">Coins</div>
908
  <div class="ppkg-p">${p.price}</div>
 
914
  document.getElementById('ppkg'+i).classList.add('sel');
915
  _selPkg=_pkgs[i];
916
  document.getElementById('pinfo').textContent=`โœ… ${_selPkg.coins} Coins โ€” ${_selPkg.price} แ€›แ€ฝแ€ฑแ€ธแ€แ€ปแ€šแ€บแ€‘แ€ฌแ€ธแ€žแ€Šแ€บ`;
917
+ document.getElementById('pinfo').style.background='rgba(139,92,246,.15)';
918
+ document.getElementById('pinfo').style.borderColor='rgba(167,139,250,.35)';
919
  checkPReady();
920
  }
921
 
 
980
  <span class="ph-badge ${p.status}">${lbl[p.status]||p.status}</span>
981
  </div>
982
  <div class="ph-meta">๐Ÿ†” ${p.id} &nbsp;ยท&nbsp; ${p.created_at.slice(0,16).replace('T',' ')}</div>
983
+ ${p.admin_note?`<div style="margin-top:6px;font-size:.72rem;color:rgba(255,255,255,.35);padding:6px 10px;background:rgba(255,255,255,.04);border-radius:7px">๐Ÿ“ ${p.admin_note}</div>`:''}
984
  </div>`).join('');
985
  }catch(e){ el.innerHTML=`<div class="ph-empty">โŒ ${e.message}</div>`; }
986
  }
 
1274
  async function loadUsers(){const r=await fetch('/api/admin/users?caller='+encodeURIComponent(U));if(!r.ok)return;const d=await r.json();if(!d.ok)return;const w=document.getElementById('utw');if(!d.users.length){w.innerHTML='<div style="font-size:.75rem;color:var(--muted);padding:6px">No users</div>';return;}let h='<table class="ut"><thead><tr><th>Username</th><th>Coins</th><th>Videos</th><th>Created</th><th></th></tr></thead><tbody>';d.users.forEach(u=>{h+=`<tr><td>${u.username}</td><td>๐Ÿช™${u.coins}</td><td>${u.videos}</td><td>${u.created||''}</td><td><button class="delbtn" onclick="qDel('${u.username}')"><i class="fas fa-trash"></i></button></td></tr>`;});w.innerHTML=h+'</tbody></table>';}
1275
  async function qDel(u){if(!confirm('Delete '+u+'?'))return;const r=await fetch('/api/admin/delete_user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,username:u})});const d=await r.json();toast(d.msg);if(d.ok)loadUsers();}
1276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
  /* โ•โ• UTILS โ•โ• */
1278
  function toast(m){const e=document.getElementById('toast');e.textContent=m;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),2800);}
1279
 
 
1299
  <!-- โ•โ• PAYMENT MODAL โ•โ• -->
1300
  <div class="pov" id="pov" onclick="if(event.target===this)closePay()">
1301
  <div class="psheet">
1302
+
1303
+ <!-- Header -->
1304
  <div class="psheet-hdr">
1305
+ <h2><span>๐Ÿช™</span> Coins แ€แ€šแ€บแ€›แ€”แ€บ</h2>
1306
  <button class="pclose" onclick="closePay()"><i class="fas fa-times"></i></button>
1307
  </div>
1308
+
1309
+ <!-- Tabs -->
1310
  <div class="tab2">
1311
  <button id="ptab-buy" class="on" onclick="showPayTab('buy')">๐Ÿ›’ แ€แ€šแ€บแ€›แ€”แ€บ</button>
1312
  <button id="ptab-his" onclick="showPayTab('his')">๐Ÿ“‹ แ€™แ€พแ€แ€บแ€แ€™แ€บแ€ธ</button>
 
1314
 
1315
  <!-- BUY TAB -->
1316
  <div id="ptab-buy-content">
1317
+
1318
+ <!-- Package grid -->
1319
  <div class="ppkgs" id="ppkgs"></div>
1320
+
1321
+ <!-- Selected info -->
1322
  <div class="pinfo" id="pinfo">Package แ€แ€…แ€บแ€แ€ฏ แ€›แ€ฝแ€ฑแ€ธแ€แ€ปแ€šแ€บแ€•แ€ซ</div>
1323
+
1324
+ <!-- KBZ Pay -->
1325
  <div class="pkbz">
1326
  <div class="pkbz-t">๐Ÿ“ฑ KBZ Pay / Wave / AYA Pay</div>
1327
  <div class="pkbz-row">
1328
  <span class="pkbz-lbl">Name</span>
1329
+ <div style="display:flex;align-items:center;gap:8px">
1330
  <span class="pkbz-val" id="p-kbz-name">โ€”</span>
1331
+ <button class="pcopy" onclick="pcopy('p-kbz-name')"><i class="fas fa-copy"></i> Copy</button>
1332
  </div>
1333
  </div>
1334
  <div class="pkbz-row">
1335
  <span class="pkbz-lbl">Phone</span>
1336
+ <div style="display:flex;align-items:center;gap:8px">
1337
  <span class="pkbz-val" id="p-kbz-num">โ€”</span>
1338
+ <button class="pcopy" onclick="pcopy('p-kbz-num')"><i class="fas fa-copy"></i> Copy</button>
1339
  </div>
1340
  </div>
1341
  </div>
1342
+
1343
+ <!-- SCB Bank -->
1344
+ <div class="pkbz" style="margin-bottom:0">
1345
  <div class="pkbz-t">๐Ÿฆ SCB Bank</div>
1346
  <div class="pkbz-row">
1347
  <span class="pkbz-lbl">Name</span>
1348
+ <div style="display:flex;align-items:center;gap:8px">
1349
  <span class="pkbz-val" id="p-scb-name">โ€”</span>
1350
+ <button class="pcopy" onclick="pcopy('p-scb-name')"><i class="fas fa-copy"></i> Copy</button>
1351
  </div>
1352
  </div>
1353
  <div class="pkbz-row">
1354
  <span class="pkbz-lbl">Account</span>
1355
+ <div style="display:flex;align-items:center;gap:8px">
1356
  <span class="pkbz-val" id="p-scb-num">โ€”</span>
1357
+ <button class="pcopy" onclick="pcopy('p-scb-num')"><i class="fas fa-copy"></i> Copy</button>
1358
  </div>
1359
  </div>
1360
  </div>
1361
+
1362
+ <!-- Slip upload -->
1363
  <span class="pslip-label">๐Ÿ“ธ แ€„แ€ฝแ€ฑแ€œแ€ฝแ€พแ€ฒ Slip แ€•แ€ฏแ€ถ แ€แ€„แ€บแ€•แ€ฑแ€ธแ€•แ€ซ</span>
1364
  <div class="pslip-drop" id="pslip-drop">
1365
  <input type="file" accept="image/*" id="pslip-input" onchange="handlePSlip(this)">
1366
  <div class="pi" id="pslip-icon">๐Ÿ“Ž</div>
1367
+ <div class="pt" id="pslip-txt">Slip แ€•แ€ฏแ€ถแ€€แ€ญแ€ฏ แ€’แ€ฎแ€”แ€ฑแ€›แ€ฌ แ€–แ€ญแ€†แ€ฝแ€ฒแ€แ€ปแ€•แ€ซ<br><span style="color:rgba(255,255,255,.2);font-size:.7rem">แ€žแ€ญแ€ฏแ€ทแ€™แ€Ÿแ€ฏแ€แ€บ แ€”แ€พแ€ญแ€•แ€บแ€•แ€ผแ€ฎแ€ธ แ€›แ€ฝแ€ฑแ€ธแ€•แ€ซ</span></div>
1368
  <img id="pslip-prev" class="pslip-preview">
1369
  </div>
1370
+
1371
+ <!-- Submit -->
1372
+ <button class="psubmit" id="psubmit" onclick="submitPay()" disabled>
1373
+ <i class="fas fa-paper-plane" style="margin-right:8px"></i>Payment แ€แ€„แ€บแ€•แ€ฑแ€ธแ€•แ€ซ
1374
+ </button>
1375
+
1376
  </div>
1377
 
1378
  <!-- HISTORY TAB -->
1379
  <div id="ptab-his-content" style="display:none">
1380
+ <div class="ph-list-wrap">
1381
+ <div id="ph-list"><div class="ph-empty">โณ Loadingโ€ฆ</div></div>
1382
+ </div>
1383
  </div>
1384
+
1385
  </div>
1386
  </div>
1387
 
start.sh CHANGED
@@ -15,6 +15,15 @@ cleanup() {
15
  }
16
  trap cleanup SIGTERM SIGINT
17
 
 
 
 
 
 
 
 
 
 
18
  echo "๐ŸŒ Starting Gunicorn on port $PORT (workers=$WORKERS, threads=$THREADS)..."
19
  gunicorn app:app \
20
  --bind "0.0.0.0:$PORT" \
 
15
  }
16
  trap cleanup SIGTERM SIGINT
17
 
18
+ # โ”€โ”€ Clear any stale bot sessions from Telegram โ”€โ”€
19
+ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
20
+ echo "๐Ÿ”„ Clearing Telegram webhook + stale sessions..."
21
+ # Delete webhook (in case webhook mode was ever used)
22
+ curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook?drop_pending_updates=true" > /dev/null || true
23
+ # Small delay to let Telegram close any existing getUpdates long-polls
24
+ sleep 2
25
+ fi
26
+
27
  echo "๐ŸŒ Starting Gunicorn on port $PORT (workers=$WORKERS, threads=$THREADS)..."
28
  gunicorn app:app \
29
  --bind "0.0.0.0:$PORT" \