Phoe2004 commited on
Commit
d697f22
ยท
verified ยท
1 Parent(s): cc83ea3

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +840 -150
app.py CHANGED
@@ -320,6 +320,34 @@ def api_cookie_check():
320
  'keys': keys,
321
  })
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  def get_latest_youtube_video(channel_id):
324
  """Use yt-dlp to get latest video ID & title โ€” bypasses RSS cache delay"""
325
  try:
@@ -364,7 +392,10 @@ def get_latest_youtube_video(channel_id):
364
  def auto_poster_worker():
365
  global bot_running
366
  add_log('๐Ÿค– Auto Poster started')
367
- CHANNEL_ID = 'UCsgopUB269PWGYTXo-MXjpA'
 
 
 
368
  last_id = None
369
 
370
  while bot_running:
@@ -402,170 +433,829 @@ def auto_poster_worker():
402
  # โ”€โ”€ HTML Template โ”€โ”€
403
  HTML_TEMPLATE = '''
404
  <!DOCTYPE html>
405
- <html>
406
  <head>
407
- <title>YouTube to TikTok Bot</title>
408
- <style>
409
- body { font-family: monospace; max-width: 820px; margin: 0 auto; padding: 20px; background: #0a0a0a; color: #0f0; }
410
- .card { background: #111; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 20px; }
411
- button { background: #00aa00; color: black; border: none; padding: 8px 16px; cursor: pointer; margin-right: 8px; border-radius: 4px; }
412
- button:hover { background: #00cc00; }
413
- button.red { background: #aa0000; color: #fff; }
414
- button.red:hover { background: #cc0000; }
415
- button.gray { background: #444; color: #fff; }
416
- input, select, textarea { padding: 8px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #222; color: #0f0; border: 1px solid #333; border-radius: 4px; }
417
- .log-box { height: 220px; overflow-y: scroll; background: #050505; border: 1px solid #333; padding: 10px; font-size: 11px; line-height: 1.6; }
418
- .status { padding: 8px 12px; border-radius: 4px; margin-bottom: 10px; font-weight: bold; }
419
- .status.running { background: #1a3a1a; color: #0f0; }
420
- .status.stopped { background: #3a1a1a; color: #f55; }
421
- hr { border-color: #333; margin: 12px 0; }
422
- .cookie-info { font-size: 11px; color: #888; margin-top: 6px; }
423
- .cookie-info span { color: #0f0; }
424
- .cookie-info span.bad { color: #f55; }
425
- h2 { margin: 0 0 12px 0; font-size: 15px; }
426
- h1 { font-size: 20px; }
427
- .result { margin-top: 8px; font-size: 12px; min-height: 18px; }
428
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  </head>
430
  <body>
431
- <h1>๐ŸŽฌ YouTube โ†’ TikTok Bot</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
- <!-- Cookie Status -->
434
- <div class="card">
435
- <h2>๐Ÿช Cookie Status</h2>
436
- <div id="cookie-status" class="cookie-info">Loading...</div>
437
- <button class="gray" onclick="checkCookie()" style="margin-top:8px;">๐Ÿ” Check Cookies</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  </div>
439
 
440
- <!-- Auto Bot -->
441
- <div class="card">
442
- <h2>๐Ÿค– Auto Poster Bot</h2>
443
- <div id="status" class="status stopped">๐Ÿ”ด STOPPED</div>
444
- <button onclick="startBot()">โ–ถ๏ธ START</button>
445
- <button class="red" onclick="stopBot()">โน๏ธ STOP</button>
446
- <button class="gray" onclick="refreshStatus()">๐Ÿ”„ Refresh</button>
447
- <hr>
448
- <div id="log" class="log-box"></div>
 
 
 
 
 
449
  </div>
450
 
451
- <!-- Manual Upload by URL -->
452
- <div class="card">
453
- <h2>๐Ÿ“ค Manual Upload (by URL)</h2>
454
- <input type="text" id="manual-url" placeholder="YouTube / Video URL">
455
- <input type="text" id="manual-title" placeholder="Video Title (caption)">
456
- <select id="manual-quality">
 
 
 
 
 
 
 
 
 
457
  <option value="1080">1080p</option>
458
  <option value="720" selected>720p</option>
459
  <option value="480">480p</option>
460
- </select>
461
- <button onclick="manualUpload()">โฌ†๏ธ Download & Upload to TikTok</button>
462
- <div id="manual-result" class="result"></div>
 
 
 
 
 
 
463
  </div>
464
 
465
- <!-- Manual Upload by File -->
466
- <div class="card">
467
- <h2>๐Ÿ“ Manual Upload (by File)</h2>
468
- <input type="file" id="file-input" accept="video/mp4,video/*">
469
- <input type="text" id="file-title" placeholder="Video Title (caption)">
470
- <button onclick="manualUploadFile()">โฌ†๏ธ Upload File to TikTok</button>
471
- <div id="file-result" class="result"></div>
 
 
 
 
 
 
 
 
472
  </div>
473
-
474
- <script>
475
- async function checkCookie() {
476
- const res = await fetch('/api/cookie/check');
477
- const d = await res.json();
478
- const el = document.getElementById('cookie-status');
479
- const ok = v => `<span>${v ? 'โœ…' : '<span class="bad">โŒ</span>'}</span>`;
480
- el.innerHTML = `
481
- sessionid: ${ok(d.has_sessionid)} &nbsp;|&nbsp;
482
- msToken: ${ok(d.has_msToken)} &nbsp;|&nbsp;
483
- csrf: ${ok(d.has_csrf)} &nbsp;|&nbsp;
484
- Total keys: <span>${d.total_keys}</span>
485
- `;
486
- }
487
-
488
- async function refreshStatus() {
489
- const res = await fetch('/api/bot/status');
490
- const d = await res.json();
491
- const statusDiv = document.getElementById('status');
492
- statusDiv.className = 'status ' + (d.running ? 'running' : 'stopped');
493
- statusDiv.innerHTML = d.running ? '๐ŸŸข RUNNING' : '๐Ÿ”ด STOPPED';
494
- const logEl = document.getElementById('log');
495
- logEl.innerHTML = d.logs.map(l => `<div>${escapeHtml(l)}</div>`).join('');
496
- logEl.scrollTop = logEl.scrollHeight;
497
- }
498
-
499
- async function startBot() {
500
- const res = await fetch('/api/bot/start', {method: 'POST'});
501
- const d = await res.json();
502
- alert(d.msg);
503
- refreshStatus();
504
- }
505
-
506
- async function stopBot() {
507
- const res = await fetch('/api/bot/stop', {method: 'POST'});
508
- const d = await res.json();
509
- alert(d.msg);
510
- refreshStatus();
511
- }
512
-
513
- async function manualUpload() {
514
- const url = document.getElementById('manual-url').value.trim();
515
- const title = document.getElementById('manual-title').value.trim();
516
- const quality = document.getElementById('manual-quality').value;
517
- if (!url || !title) { alert('URL แ€”แ€ฒแ€ท Title แ€‘แ€Šแ€ทแ€บแ€•แ€ซ'); return; }
518
-
519
- const el = document.getElementById('manual-result');
520
- el.innerHTML = 'โณ Downloading & uploading... (แ€แ€แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€•แ€ซ)';
521
- try {
522
- const res = await fetch('/api/upload/manual', {
523
- method: 'POST',
524
- headers: {'Content-Type': 'application/json'},
525
- body: JSON.stringify({url, title, quality})
526
- });
527
- const d = await res.json();
528
- el.innerHTML = d.ok ? 'โœ… ' + d.msg : 'โŒ ' + d.msg;
529
- } catch(e) {
530
- el.innerHTML = 'โŒ Request error: ' + e;
531
- }
532
- refreshStatus();
533
- }
534
-
535
- async function manualUploadFile() {
536
- const file = document.getElementById('file-input').files[0];
537
- const title = document.getElementById('file-title').value.trim();
538
- if (!file || !title) { alert('File แ€›แ€ฝแ€ฑแ€ธแ€•แ€ผแ€ฎแ€ธ Title แ€‘แ€Šแ€ทแ€บแ€•แ€ซ'); return; }
539
-
540
- const el = document.getElementById('file-result');
541
- el.innerHTML = 'โณ Uploading file... (แ€แ€แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€•แ€ซ)';
542
- const formData = new FormData();
543
- formData.append('video', file);
544
- formData.append('title', title);
545
- try {
546
- const res = await fetch('/api/upload/file', {method: 'POST', body: formData});
547
- const d = await res.json();
548
- el.innerHTML = d.ok ? 'โœ… ' + d.msg : 'โŒ ' + d.msg;
549
- } catch(e) {
550
- el.innerHTML = 'โŒ Request error: ' + e;
551
- }
552
- refreshStatus();
553
- }
554
-
555
- function escapeHtml(text) {
556
- const div = document.createElement('div');
557
- div.textContent = text;
558
- return div.innerHTML;
559
- }
560
-
561
- // Init
562
- checkCookie();
563
- refreshStatus();
564
- setInterval(refreshStatus, 5000);
565
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  </body>
567
  </html>
 
568
  '''
569
 
570
- if __name__ == '__main__':
571
- app.run(host='0.0.0.0', port=7860, debug=False)
 
320
  'keys': keys,
321
  })
322
 
323
+ @app.route('/api/settings/save', methods=['POST'])
324
+ def api_settings_save():
325
+ """Save channel_id and/or cookies.txt from the UI"""
326
+ data = request.get_json() or {}
327
+ channel_id = data.get('channel_id', '').strip()
328
+ cookie_txt = data.get('cookie_txt', '').strip()
329
+ msgs = []
330
+
331
+ if channel_id:
332
+ # Hot-patch the worker's CHANNEL_ID by writing to a sidecar file
333
+ try:
334
+ (BASE_DIR / 'channel_id.txt').write_text(channel_id)
335
+ msgs.append(f'Channel ID saved: {channel_id}')
336
+ except Exception as e:
337
+ return jsonify({'ok': False, 'msg': f'Channel ID save error: {e}'})
338
+
339
+ if cookie_txt:
340
+ try:
341
+ Path(TIKTOK_COOKIE_FILE).write_text(cookie_txt)
342
+ msgs.append('cookies.txt updated')
343
+ except Exception as e:
344
+ return jsonify({'ok': False, 'msg': f'Cookie save error: {e}'})
345
+
346
+ if not msgs:
347
+ return jsonify({'ok': False, 'msg': 'Nothing provided'})
348
+ return jsonify({'ok': True, 'msg': ' ยท '.join(msgs)})
349
+
350
+
351
  def get_latest_youtube_video(channel_id):
352
  """Use yt-dlp to get latest video ID & title โ€” bypasses RSS cache delay"""
353
  try:
 
392
  def auto_poster_worker():
393
  global bot_running
394
  add_log('๐Ÿค– Auto Poster started')
395
+ # Read channel_id from file if available (set via UI)
396
+ _cid_file = BASE_DIR / 'channel_id.txt'
397
+ CHANNEL_ID = _cid_file.read_text().strip() if _cid_file.exists() else 'UCsgopUB269PWGYTXo-MXjpA'
398
+ add_log(f'๐Ÿ“ก Monitoring channel: {CHANNEL_ID}')
399
  last_id = None
400
 
401
  while bot_running:
 
433
  # โ”€โ”€ HTML Template โ”€โ”€
434
  HTML_TEMPLATE = '''
435
  <!DOCTYPE html>
436
+ <html lang="en">
437
  <head>
438
+ <meta charset="UTF-8">
439
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
440
+ <title>YT โ†’ TikTok AutoBot</title>
441
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
442
+ <style>
443
+ :root {
444
+ --bg: #080810;
445
+ --bg2: #0d0d1a;
446
+ --card: #10101e;
447
+ --border: #1e1e3a;
448
+ --accent: #ff2d55;
449
+ --accent2: #00f0ff;
450
+ --accent3: #7b5ea7;
451
+ --green: #00e676;
452
+ --yellow: #ffd600;
453
+ --text: #e8e8f0;
454
+ --muted: #555570;
455
+ --font-display: 'Syne', sans-serif;
456
+ --font-mono: 'Space Mono', monospace;
457
+ }
458
+
459
+ * { margin: 0; padding: 0; box-sizing: border-box; }
460
+
461
+ body {
462
+ background: var(--bg);
463
+ color: var(--text);
464
+ font-family: var(--font-mono);
465
+ min-height: 100vh;
466
+ overflow-x: hidden;
467
+ }
468
+
469
+ /* Grid bg */
470
+ body::before {
471
+ content: '';
472
+ position: fixed;
473
+ inset: 0;
474
+ background-image:
475
+ linear-gradient(rgba(0,240,255,0.03) 1px, transparent 1px),
476
+ linear-gradient(90deg, rgba(0,240,255,0.03) 1px, transparent 1px);
477
+ background-size: 40px 40px;
478
+ pointer-events: none;
479
+ z-index: 0;
480
+ }
481
+
482
+ /* Glow orbs */
483
+ .orb {
484
+ position: fixed;
485
+ border-radius: 50%;
486
+ filter: blur(120px);
487
+ pointer-events: none;
488
+ z-index: 0;
489
+ }
490
+ .orb1 { width: 500px; height: 500px; background: rgba(255,45,85,0.08); top: -150px; right: -100px; }
491
+ .orb2 { width: 400px; height: 400px; background: rgba(0,240,255,0.06); bottom: 100px; left: -100px; }
492
+ .orb3 { width: 300px; height: 300px; background: rgba(123,94,167,0.07); top: 40%; left: 40%; }
493
+
494
+ .wrap {
495
+ position: relative;
496
+ z-index: 1;
497
+ max-width: 860px;
498
+ margin: 0 auto;
499
+ padding: 28px 20px 60px;
500
+ }
501
+
502
+ /* Header */
503
+ .header {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: 16px;
507
+ margin-bottom: 36px;
508
+ padding-bottom: 20px;
509
+ border-bottom: 1px solid var(--border);
510
+ }
511
+ .logo-mark {
512
+ width: 46px; height: 46px;
513
+ background: linear-gradient(135deg, var(--accent), var(--accent3));
514
+ border-radius: 12px;
515
+ display: flex; align-items: center; justify-content: center;
516
+ font-size: 20px;
517
+ flex-shrink: 0;
518
+ box-shadow: 0 0 20px rgba(255,45,85,0.3);
519
+ }
520
+ .header-text h1 {
521
+ font-family: var(--font-display);
522
+ font-size: 22px;
523
+ font-weight: 800;
524
+ letter-spacing: -0.5px;
525
+ background: linear-gradient(90deg, #fff 40%, var(--accent2));
526
+ -webkit-background-clip: text;
527
+ -webkit-text-fill-color: transparent;
528
+ }
529
+ .header-text p {
530
+ font-size: 11px;
531
+ color: var(--muted);
532
+ margin-top: 2px;
533
+ letter-spacing: 1px;
534
+ text-transform: uppercase;
535
+ }
536
+ .header-badge {
537
+ margin-left: auto;
538
+ font-size: 10px;
539
+ color: var(--accent2);
540
+ border: 1px solid rgba(0,240,255,0.3);
541
+ padding: 4px 10px;
542
+ border-radius: 20px;
543
+ letter-spacing: 1px;
544
+ text-transform: uppercase;
545
+ }
546
+
547
+ /* Cards */
548
+ .card {
549
+ background: var(--card);
550
+ border: 1px solid var(--border);
551
+ border-radius: 16px;
552
+ padding: 22px 24px;
553
+ margin-bottom: 16px;
554
+ position: relative;
555
+ overflow: hidden;
556
+ transition: border-color 0.2s;
557
+ }
558
+ .card::before {
559
+ content: '';
560
+ position: absolute;
561
+ top: 0; left: 0; right: 0;
562
+ height: 1px;
563
+ background: linear-gradient(90deg, transparent, rgba(0,240,255,0.15), transparent);
564
+ }
565
+ .card:hover { border-color: rgba(0,240,255,0.15); }
566
+
567
+ .card-header {
568
+ display: flex;
569
+ align-items: center;
570
+ gap: 10px;
571
+ margin-bottom: 18px;
572
+ }
573
+ .card-icon {
574
+ width: 32px; height: 32px;
575
+ border-radius: 8px;
576
+ display: flex; align-items: center; justify-content: center;
577
+ font-size: 15px;
578
+ flex-shrink: 0;
579
+ }
580
+ .card-icon.red { background: rgba(255,45,85,0.15); }
581
+ .card-icon.cyan { background: rgba(0,240,255,0.12); }
582
+ .card-icon.purple { background: rgba(123,94,167,0.2); }
583
+ .card-icon.green { background: rgba(0,230,118,0.12); }
584
+ .card-icon.yellow { background: rgba(255,214,0,0.12); }
585
+
586
+ .card-title {
587
+ font-family: var(--font-display);
588
+ font-size: 13px;
589
+ font-weight: 700;
590
+ letter-spacing: 0.5px;
591
+ text-transform: uppercase;
592
+ color: #fff;
593
+ }
594
+ .card-sub {
595
+ font-size: 10px;
596
+ color: var(--muted);
597
+ margin-top: 1px;
598
+ }
599
+
600
+ /* Status pill */
601
+ .status-pill {
602
+ display: inline-flex;
603
+ align-items: center;
604
+ gap: 7px;
605
+ padding: 6px 14px;
606
+ border-radius: 30px;
607
+ font-size: 11px;
608
+ font-weight: 700;
609
+ letter-spacing: 1px;
610
+ text-transform: uppercase;
611
+ transition: all 0.3s;
612
+ }
613
+ .status-pill.running {
614
+ background: rgba(0,230,118,0.1);
615
+ border: 1px solid rgba(0,230,118,0.3);
616
+ color: var(--green);
617
+ }
618
+ .status-pill.stopped {
619
+ background: rgba(255,45,85,0.1);
620
+ border: 1px solid rgba(255,45,85,0.3);
621
+ color: var(--accent);
622
+ }
623
+ .status-dot {
624
+ width: 7px; height: 7px;
625
+ border-radius: 50%;
626
+ background: currentColor;
627
+ }
628
+ .status-dot.pulse {
629
+ animation: pulse 1.5s infinite;
630
+ }
631
+ @keyframes pulse {
632
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }
633
+ 50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
634
+ }
635
+
636
+ /* Bot control row */
637
+ .bot-row {
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 12px;
641
+ flex-wrap: wrap;
642
+ }
643
+
644
+ /* Buttons */
645
+ .btn {
646
+ display: inline-flex;
647
+ align-items: center;
648
+ gap: 6px;
649
+ padding: 9px 18px;
650
+ border-radius: 8px;
651
+ border: none;
652
+ font-family: var(--font-mono);
653
+ font-size: 11px;
654
+ font-weight: 700;
655
+ letter-spacing: 0.5px;
656
+ cursor: pointer;
657
+ transition: all 0.15s;
658
+ text-transform: uppercase;
659
+ }
660
+ .btn:active { transform: scale(0.97); }
661
+ .btn-start {
662
+ background: linear-gradient(135deg, #00b341, #00e676);
663
+ color: #000;
664
+ box-shadow: 0 0 16px rgba(0,230,118,0.25);
665
+ }
666
+ .btn-start:hover { box-shadow: 0 0 24px rgba(0,230,118,0.4); }
667
+ .btn-stop {
668
+ background: linear-gradient(135deg, #c0002a, #ff2d55);
669
+ color: #fff;
670
+ box-shadow: 0 0 16px rgba(255,45,85,0.25);
671
+ }
672
+ .btn-stop:hover { box-shadow: 0 0 24px rgba(255,45,85,0.4); }
673
+ .btn-ghost {
674
+ background: transparent;
675
+ color: var(--muted);
676
+ border: 1px solid var(--border);
677
+ }
678
+ .btn-ghost:hover { border-color: var(--accent2); color: var(--accent2); }
679
+ .btn-cyan {
680
+ background: linear-gradient(135deg, #007a87, #00f0ff);
681
+ color: #000;
682
+ box-shadow: 0 0 16px rgba(0,240,255,0.2);
683
+ }
684
+ .btn-cyan:hover { box-shadow: 0 0 24px rgba(0,240,255,0.35); }
685
+ .btn-purple {
686
+ background: linear-gradient(135deg, #4a2a7a, #7b5ea7);
687
+ color: #fff;
688
+ box-shadow: 0 0 16px rgba(123,94,167,0.25);
689
+ }
690
+ .btn-purple:hover { box-shadow: 0 0 24px rgba(123,94,167,0.4); }
691
+ .btn-sm { padding: 7px 14px; font-size: 10px; }
692
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
693
+
694
+ /* Divider */
695
+ .divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
696
+
697
+ /* Log box */
698
+ .log-wrap { position: relative; }
699
+ .log-box {
700
+ height: 200px;
701
+ overflow-y: auto;
702
+ background: #07070f;
703
+ border: 1px solid var(--border);
704
+ border-radius: 10px;
705
+ padding: 12px 14px;
706
+ font-size: 11px;
707
+ line-height: 1.8;
708
+ color: #8888aa;
709
+ scrollbar-width: thin;
710
+ scrollbar-color: var(--border) transparent;
711
+ }
712
+ .log-box::-webkit-scrollbar { width: 4px; }
713
+ .log-box::-webkit-scrollbar-track { background: transparent; }
714
+ .log-box::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
715
+ .log-entry { display: flex; gap: 8px; }
716
+ .log-time { color: var(--muted); flex-shrink: 0; }
717
+ .log-msg { color: #aaaacc; }
718
+ .log-msg.ok { color: var(--green); }
719
+ .log-msg.err { color: var(--accent); }
720
+ .log-msg.info { color: var(--accent2); }
721
+ .log-msg.warn { color: var(--yellow); }
722
+
723
+ /* Inputs */
724
+ .field { margin-bottom: 12px; }
725
+ .field label {
726
+ display: block;
727
+ font-size: 10px;
728
+ color: var(--muted);
729
+ letter-spacing: 1px;
730
+ text-transform: uppercase;
731
+ margin-bottom: 6px;
732
+ }
733
+ .field input, .field select, .field textarea {
734
+ width: 100%;
735
+ background: #0a0a18;
736
+ border: 1px solid var(--border);
737
+ border-radius: 8px;
738
+ padding: 10px 13px;
739
+ color: var(--text);
740
+ font-family: var(--font-mono);
741
+ font-size: 12px;
742
+ outline: none;
743
+ transition: border-color 0.2s, box-shadow 0.2s;
744
+ }
745
+ .field input:focus, .field select:focus, .field textarea:focus {
746
+ border-color: rgba(0,240,255,0.35);
747
+ box-shadow: 0 0 0 3px rgba(0,240,255,0.06);
748
+ }
749
+ .field textarea { resize: vertical; min-height: 90px; font-size: 11px; }
750
+ .field select option { background: #0d0d1a; }
751
+ .field input::placeholder { color: var(--muted); }
752
+
753
+ /* Row layouts */
754
+ .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
755
+ @media (max-width: 560px) { .row { grid-template-columns: 1fr; } }
756
+
757
+ /* Cookie chips */
758
+ .cookie-grid {
759
+ display: flex;
760
+ gap: 8px;
761
+ flex-wrap: wrap;
762
+ margin-top: 4px;
763
+ }
764
+ .chip {
765
+ display: inline-flex;
766
+ align-items: center;
767
+ gap: 5px;
768
+ padding: 4px 10px;
769
+ border-radius: 20px;
770
+ font-size: 10px;
771
+ font-weight: 700;
772
+ letter-spacing: 0.5px;
773
+ border: 1px solid transparent;
774
+ }
775
+ .chip.ok { background: rgba(0,230,118,0.08); border-color: rgba(0,230,118,0.25); color: var(--green); }
776
+ .chip.bad { background: rgba(255,45,85,0.08); border-color: rgba(255,45,85,0.25); color: var(--accent); }
777
+ .chip.neutral { background: rgba(255,214,0,0.08); border-color: rgba(255,214,0,0.25); color: var(--yellow); }
778
+
779
+ /* Result message */
780
+ .result-msg {
781
+ margin-top: 10px;
782
+ font-size: 11px;
783
+ min-height: 16px;
784
+ padding: 0 2px;
785
+ transition: all 0.2s;
786
+ }
787
+ .result-msg.ok { color: var(--green); }
788
+ .result-msg.err { color: var(--accent); }
789
+ .result-msg.loading { color: var(--accent2); }
790
+
791
+ /* Section label */
792
+ .section-tag {
793
+ display: inline-block;
794
+ font-size: 9px;
795
+ letter-spacing: 2px;
796
+ text-transform: uppercase;
797
+ color: var(--accent2);
798
+ border-left: 2px solid var(--accent2);
799
+ padding-left: 8px;
800
+ margin-bottom: 14px;
801
+ opacity: 0.7;
802
+ }
803
+
804
+ /* Tabs */
805
+ .tabs { display: flex; gap: 4px; margin-bottom: 18px; }
806
+ .tab {
807
+ flex: 1;
808
+ padding: 9px;
809
+ background: #0a0a18;
810
+ border: 1px solid var(--border);
811
+ border-radius: 8px;
812
+ color: var(--muted);
813
+ font-family: var(--font-mono);
814
+ font-size: 10px;
815
+ font-weight: 700;
816
+ letter-spacing: 0.5px;
817
+ text-transform: uppercase;
818
+ cursor: pointer;
819
+ text-align: center;
820
+ transition: all 0.15s;
821
+ }
822
+ .tab.active {
823
+ background: rgba(0,240,255,0.08);
824
+ border-color: rgba(0,240,255,0.3);
825
+ color: var(--accent2);
826
+ }
827
+ .tab-panel { display: none; }
828
+ .tab-panel.active { display: block; }
829
+
830
+ /* File drop zone */
831
+ .dropzone {
832
+ border: 2px dashed var(--border);
833
+ border-radius: 10px;
834
+ padding: 24px;
835
+ text-align: center;
836
+ cursor: pointer;
837
+ transition: all 0.2s;
838
+ background: #07070f;
839
+ margin-bottom: 12px;
840
+ }
841
+ .dropzone:hover, .dropzone.drag { border-color: rgba(0,240,255,0.4); background: rgba(0,240,255,0.03); }
842
+ .dropzone input { display: none; }
843
+ .dropzone .dz-icon { font-size: 28px; margin-bottom: 8px; }
844
+ .dropzone .dz-text { font-size: 11px; color: var(--muted); }
845
+ .dropzone .dz-file { font-size: 11px; color: var(--accent2); margin-top: 4px; }
846
+
847
+ /* Settings save feedback */
848
+ .save-badge {
849
+ display: inline-flex;
850
+ align-items: center;
851
+ gap: 5px;
852
+ font-size: 10px;
853
+ color: var(--green);
854
+ opacity: 0;
855
+ transition: opacity 0.3s;
856
+ margin-left: 8px;
857
+ }
858
+ .save-badge.show { opacity: 1; }
859
+
860
+ /* Spinner */
861
+ .spin {
862
+ display: inline-block;
863
+ width: 11px; height: 11px;
864
+ border: 2px solid rgba(0,240,255,0.2);
865
+ border-top-color: var(--accent2);
866
+ border-radius: 50%;
867
+ animation: spin 0.7s linear infinite;
868
+ vertical-align: middle;
869
+ margin-right: 4px;
870
+ }
871
+ @keyframes spin { to { transform: rotate(360deg); } }
872
+
873
+ /* Fade in */
874
+ @keyframes fadeUp {
875
+ from { opacity: 0; transform: translateY(12px); }
876
+ to { opacity: 1; transform: translateY(0); }
877
+ }
878
+ .card { animation: fadeUp 0.4s ease both; }
879
+ .card:nth-child(1) { animation-delay: 0.05s; }
880
+ .card:nth-child(2) { animation-delay: 0.10s; }
881
+ .card:nth-child(3) { animation-delay: 0.15s; }
882
+ .card:nth-child(4) { animation-delay: 0.20s; }
883
+ .card:nth-child(5) { animation-delay: 0.25s; }
884
+
885
+ /* Channel ID display */
886
+ .channel-display {
887
+ font-size: 11px;
888
+ color: var(--muted);
889
+ margin-top: 6px;
890
+ }
891
+ .channel-display span { color: var(--accent2); }
892
+
893
+ /* total keys */
894
+ .total-keys {
895
+ font-size: 10px;
896
+ color: var(--muted);
897
+ margin-top: 8px;
898
+ }
899
+ .total-keys b { color: var(--yellow); }
900
+ </style>
901
  </head>
902
  <body>
903
+ <div class="orb orb1"></div>
904
+ <div class="orb orb2"></div>
905
+ <div class="orb orb3"></div>
906
+
907
+ <div class="wrap">
908
+
909
+ <!-- Header -->
910
+ <div class="header">
911
+ <div class="logo-mark">๐ŸŽฌ</div>
912
+ <div class="header-text">
913
+ <h1>YT โ†’ TikTok AutoBot</h1>
914
+ <p>Automated cross-platform poster</p>
915
+ </div>
916
+ <div class="header-badge" id="conn-badge">โ— LIVE</div>
917
+ </div>
918
+
919
+ <!-- โ‘  Bot Control -->
920
+ <div class="card">
921
+ <div class="card-header">
922
+ <div class="card-icon red">๐Ÿค–</div>
923
+ <div>
924
+ <div class="card-title">Auto Poster Bot</div>
925
+ <div class="card-sub">Monitors YouTube channel ยท posts to TikTok</div>
926
+ </div>
927
+ <div style="margin-left:auto;" id="status-pill">
928
+ <span class="status-pill stopped"><span class="status-dot"></span> Stopped</span>
929
+ </div>
930
+ </div>
931
+
932
+ <div class="bot-row">
933
+ <button class="btn btn-start" onclick="startBot()">โ–ถ Start</button>
934
+ <button class="btn btn-stop" onclick="stopBot()">โ–  Stop</button>
935
+ <button class="btn btn-ghost btn-sm" onclick="refreshStatus()">โ†ป Refresh</button>
936
+ </div>
937
 
938
+ <hr class="divider">
939
+ <div class="log-wrap">
940
+ <div class="log-box" id="log-box">
941
+ <div class="log-entry"><span class="log-msg info">โ€” Waiting for logs โ€”</span></div>
942
+ </div>
943
+ </div>
944
+ </div>
945
+
946
+ <!-- โ‘ก Settings -->
947
+ <div class="card">
948
+ <div class="card-header">
949
+ <div class="card-icon purple">โš™๏ธ</div>
950
+ <div>
951
+ <div class="card-title">Settings</div>
952
+ <div class="card-sub">Channel ID ยท Cookie configuration</div>
953
+ </div>
954
+ </div>
955
+
956
+ <div class="section-tag">YouTube Channel</div>
957
+ <div class="field">
958
+ <label>Channel ID</label>
959
+ <input type="text" id="channel-id-input" placeholder="UCxxxxxxxxxxxxxxxxxxxxxxxx"
960
+ value="UCsgopUB269PWGYTXo-MXjpA">
961
+ </div>
962
+ <div class="channel-display" id="channel-preview">
963
+ Channel: <span id="channel-url-preview">https://youtube.com/channel/UCsgopUB269PWGYTXo-MXjpA</span>
964
+ </div>
965
+
966
+ <hr class="divider">
967
+ <div class="section-tag">TikTok Cookies</div>
968
+ <div class="field">
969
+ <label>cookies.txt content (Netscape or one-line format)</label>
970
+ <textarea id="cookie-text-input" placeholder="# Netscape HTTP Cookie File&#10;.tiktok.com TRUE / FALSE 2147483647 sessionid xxxxx&#10;&#10;โ€” or one-line: โ€”&#10;sessionid=xxx; msToken=xxx; tt_csrf_token=xxx"></textarea>
971
+ </div>
972
+
973
+ <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
974
+ <button class="btn btn-purple btn-sm" onclick="saveSettings()">๐Ÿ’พ Save Settings</button>
975
+ <span class="save-badge" id="save-badge">โœ“ Saved</span>
976
+ </div>
977
+ <div class="result-msg" id="settings-result"></div>
978
+ </div>
979
+
980
+ <!-- โ‘ข Cookie Status -->
981
+ <div class="card">
982
+ <div class="card-header">
983
+ <div class="card-icon yellow">๐Ÿช</div>
984
+ <div>
985
+ <div class="card-title">Cookie Status</div>
986
+ <div class="card-sub">TikTok session validation</div>
987
+ </div>
988
+ <button class="btn btn-ghost btn-sm" style="margin-left:auto;" onclick="checkCookie()">๐Ÿ” Check</button>
989
  </div>
990
 
991
+ <div class="cookie-grid" id="cookie-chips">
992
+ <span class="chip neutral">Loading...</span>
993
+ </div>
994
+ <div class="total-keys" id="cookie-total"></div>
995
+ </div>
996
+
997
+ <!-- โ‘ฃ Upload -->
998
+ <div class="card">
999
+ <div class="card-header">
1000
+ <div class="card-icon cyan">๐Ÿ“ค</div>
1001
+ <div>
1002
+ <div class="card-title">Manual Upload</div>
1003
+ <div class="card-sub">Upload directly to TikTok</div>
1004
+ </div>
1005
  </div>
1006
 
1007
+ <div class="tabs">
1008
+ <div class="tab active" onclick="switchTab('url')">๐Ÿ”— By URL</div>
1009
+ <div class="tab" onclick="switchTab('file')">๐Ÿ“ By File</div>
1010
+ </div>
1011
+
1012
+ <!-- URL tab -->
1013
+ <div class="tab-panel active" id="tab-url">
1014
+ <div class="row">
1015
+ <div class="field">
1016
+ <label>YouTube / Video URL</label>
1017
+ <input type="text" id="manual-url" placeholder="https://youtube.com/watch?v=...">
1018
+ </div>
1019
+ <div class="field">
1020
+ <label>Quality</label>
1021
+ <select id="manual-quality">
1022
  <option value="1080">1080p</option>
1023
  <option value="720" selected>720p</option>
1024
  <option value="480">480p</option>
1025
+ </select>
1026
+ </div>
1027
+ </div>
1028
+ <div class="field">
1029
+ <label>Caption / Title</label>
1030
+ <input type="text" id="manual-title" placeholder="Video title for TikTok caption">
1031
+ </div>
1032
+ <button class="btn btn-cyan" onclick="manualUpload()">โฌ† Download & Upload</button>
1033
+ <div class="result-msg" id="url-result"></div>
1034
  </div>
1035
 
1036
+ <!-- File tab -->
1037
+ <div class="tab-panel" id="tab-file">
1038
+ <div class="dropzone" id="dropzone" onclick="document.getElementById('file-input').click()"
1039
+ ondragover="dzDragOver(event)" ondragleave="dzDragLeave()" ondrop="dzDrop(event)">
1040
+ <input type="file" id="file-input" accept="video/mp4,video/*" onchange="dzFileChosen()">
1041
+ <div class="dz-icon">๐ŸŽž</div>
1042
+ <div class="dz-text">Click or drag & drop video file</div>
1043
+ <div class="dz-file" id="dz-name"></div>
1044
+ </div>
1045
+ <div class="field">
1046
+ <label>Caption / Title</label>
1047
+ <input type="text" id="file-title" placeholder="Video title for TikTok caption">
1048
+ </div>
1049
+ <button class="btn btn-cyan" onclick="manualUploadFile()">โฌ† Upload to TikTok</button>
1050
+ <div class="result-msg" id="file-result"></div>
1051
  </div>
1052
+ </div>
1053
+
1054
+ </div><!-- /wrap -->
1055
+
1056
+ <script>
1057
+ // โ”€โ”€ Tab switch โ”€โ”€
1058
+ function switchTab(t) {
1059
+ document.querySelectorAll('.tab').forEach((el,i) => {
1060
+ el.classList.toggle('active', (t==='url'?0:1)===i);
1061
+ });
1062
+ document.getElementById('tab-url').classList.toggle('active', t==='url');
1063
+ document.getElementById('tab-file').classList.toggle('active', t==='file');
1064
+ }
1065
+
1066
+ // โ”€โ”€ Channel ID preview โ”€โ”€
1067
+ document.getElementById('channel-id-input').addEventListener('input', function() {
1068
+ const id = this.value.trim();
1069
+ document.getElementById('channel-url-preview').textContent =
1070
+ id ? `https://youtube.com/channel/${id}` : 'โ€”';
1071
+ });
1072
+
1073
+ // โ”€โ”€ Dropzone โ”€โ”€
1074
+ function dzDragOver(e) { e.preventDefault(); document.getElementById('dropzone').classList.add('drag'); }
1075
+ function dzDragLeave() { document.getElementById('dropzone').classList.remove('drag'); }
1076
+ function dzDrop(e) {
1077
+ e.preventDefault();
1078
+ dzDragLeave();
1079
+ const f = e.dataTransfer.files[0];
1080
+ if (f) { document.getElementById('file-input').files = e.dataTransfer.files; dzFileChosen(); }
1081
+ }
1082
+ function dzFileChosen() {
1083
+ const f = document.getElementById('file-input').files[0];
1084
+ document.getElementById('dz-name').textContent = f ? `๐Ÿ“Ž ${f.name}` : '';
1085
+ }
1086
+
1087
+ // โ”€โ”€ Log renderer โ”€โ”€
1088
+ function renderLogs(logs) {
1089
+ const box = document.getElementById('log-box');
1090
+ if (!logs || !logs.length) {
1091
+ box.innerHTML = '<div class="log-entry"><span class="log-msg info">โ€” No logs yet โ€”</span></div>';
1092
+ return;
1093
+ }
1094
+ box.innerHTML = logs.map(l => {
1095
+ const m = l.match(/^\[(\d+:\d+:\d+)\]\s*(.*)$/);
1096
+ const time = m ? m[1] : '';
1097
+ const msg = m ? m[2] : l;
1098
+ let cls = 'log-msg';
1099
+ if (/โœ…|success|upload/.test(msg)) cls += ' ok';
1100
+ else if (/โŒ|error|fail/i.test(msg)) cls += ' err';
1101
+ else if (/๐Ÿ“Œ|๐Ÿ“น|โ–ถ|started|detected/i.test(msg)) cls += ' info';
1102
+ else if (/โš ๏ธ|warn|timeout/i.test(msg)) cls += ' warn';
1103
+ return `<div class="log-entry"><span class="log-time">${escHtml(time)}</span><span class="${cls}">${escHtml(msg)}</span></div>`;
1104
+ }).join('');
1105
+ box.scrollTop = box.scrollHeight;
1106
+ }
1107
+
1108
+ // โ”€โ”€ Status โ”€โ”€
1109
+ async function refreshStatus() {
1110
+ try {
1111
+ const d = await (await fetch('/api/bot/status')).json();
1112
+ const pill = document.getElementById('status-pill');
1113
+ if (d.running) {
1114
+ pill.innerHTML = '<span class="status-pill running"><span class="status-dot pulse"></span> Running</span>';
1115
+ } else {
1116
+ pill.innerHTML = '<span class="status-pill stopped"><span class="status-dot"></span> Stopped</span>';
1117
+ }
1118
+ renderLogs(d.logs);
1119
+ } catch(e) {
1120
+ document.getElementById('conn-badge').textContent = 'โ— OFFLINE';
1121
+ document.getElementById('conn-badge').style.color = 'var(--accent)';
1122
+ document.getElementById('conn-badge').style.borderColor = 'rgba(255,45,85,0.3)';
1123
+ }
1124
+ }
1125
+
1126
+ // โ”€โ”€ Bot controls โ”€โ”€
1127
+ async function startBot() {
1128
+ const d = await (await fetch('/api/bot/start', {method:'POST'})).json();
1129
+ showToast(d.msg);
1130
+ refreshStatus();
1131
+ }
1132
+ async function stopBot() {
1133
+ const d = await (await fetch('/api/bot/stop', {method:'POST'})).json();
1134
+ showToast(d.msg);
1135
+ refreshStatus();
1136
+ }
1137
+
1138
+ // โ”€โ”€ Cookie check โ”€โ”€
1139
+ async function checkCookie() {
1140
+ const d = await (await fetch('/api/cookie/check')).json();
1141
+ const chips = [
1142
+ { key: 'sessionid', ok: d.has_sessionid },
1143
+ { key: 'msToken', ok: d.has_msToken },
1144
+ { key: 'csrf', ok: d.has_csrf },
1145
+ ];
1146
+ document.getElementById('cookie-chips').innerHTML = chips.map(c =>
1147
+ `<span class="chip ${c.ok?'ok':'bad'}">${c.ok?'โœ“':'โœ—'} ${c.key}</span>`
1148
+ ).join('');
1149
+ document.getElementById('cookie-total').innerHTML =
1150
+ `Total keys loaded: <b>${d.total_keys}</b>`;
1151
+ }
1152
+
1153
+ // โ”€โ”€ Save settings โ”€โ”€
1154
+ async function saveSettings() {
1155
+ const channelId = document.getElementById('channel-id-input').value.trim();
1156
+ const cookieTxt = document.getElementById('cookie-text-input').value.trim();
1157
+ const el = document.getElementById('settings-result');
1158
+
1159
+ if (!channelId && !cookieTxt) {
1160
+ setResult(el, 'โš  Nothing to save', 'err'); return;
1161
+ }
1162
+
1163
+ el.innerHTML = '<span class="spin"></span> Saving...';
1164
+ el.className = 'result-msg loading';
1165
+
1166
+ try {
1167
+ const res = await fetch('/api/settings/save', {
1168
+ method: 'POST',
1169
+ headers: {'Content-Type': 'application/json'},
1170
+ body: JSON.stringify({ channel_id: channelId, cookie_txt: cookieTxt })
1171
+ });
1172
+ const d = await res.json();
1173
+ setResult(el, d.ok ? 'โœ“ ' + d.msg : 'โœ— ' + d.msg, d.ok ? 'ok' : 'err');
1174
+ if (d.ok) {
1175
+ const badge = document.getElementById('save-badge');
1176
+ badge.classList.add('show');
1177
+ setTimeout(() => badge.classList.remove('show'), 2500);
1178
+ if (cookieTxt) setTimeout(checkCookie, 500);
1179
+ }
1180
+ } catch(e) {
1181
+ setResult(el, 'โœ— Request failed: ' + e, 'err');
1182
+ }
1183
+ }
1184
+
1185
+ // โ”€โ”€ Manual URL upload โ”€โ”€
1186
+ async function manualUpload() {
1187
+ const url = document.getElementById('manual-url').value.trim();
1188
+ const title = document.getElementById('manual-title').value.trim();
1189
+ const quality = document.getElementById('manual-quality').value;
1190
+ const el = document.getElementById('url-result');
1191
+ if (!url || !title) { setResult(el, 'โš  URL แ€”แ€ฒแ€ท Title แ€‘แ€Šแ€ทแ€บแ€•แ€ซ', 'err'); return; }
1192
+
1193
+ el.innerHTML = '<span class="spin"></span> Downloading & uploading...';
1194
+ el.className = 'result-msg loading';
1195
+
1196
+ try {
1197
+ const d = await (await fetch('/api/upload/manual', {
1198
+ method:'POST', headers:{'Content-Type':'application/json'},
1199
+ body: JSON.stringify({url, title, quality})
1200
+ })).json();
1201
+ setResult(el, (d.ok?'โœ“ ':'โœ— ') + d.msg, d.ok?'ok':'err');
1202
+ } catch(e) { setResult(el, 'โœ— ' + e, 'err'); }
1203
+ refreshStatus();
1204
+ }
1205
+
1206
+ // โ”€โ”€ Manual file upload โ”€โ”€
1207
+ async function manualUploadFile() {
1208
+ const file = document.getElementById('file-input').files[0];
1209
+ const title = document.getElementById('file-title').value.trim();
1210
+ const el = document.getElementById('file-result');
1211
+ if (!file || !title) { setResult(el, 'โš  File แ€›แ€ฝแ€ฑแ€ธแ€•แ€ผแ€ฎแ€ธ Title แ€‘แ€Šแ€ทแ€บแ€•แ€ซ', 'err'); return; }
1212
+
1213
+ el.innerHTML = '<span class="spin"></span> Uploading...';
1214
+ el.className = 'result-msg loading';
1215
+
1216
+ const fd = new FormData();
1217
+ fd.append('video', file);
1218
+ fd.append('title', title);
1219
+ try {
1220
+ const d = await (await fetch('/api/upload/file', {method:'POST', body:fd})).json();
1221
+ setResult(el, (d.ok?'โœ“ ':'โœ— ') + d.msg, d.ok?'ok':'err');
1222
+ } catch(e) { setResult(el, 'โœ— ' + e, 'err'); }
1223
+ refreshStatus();
1224
+ }
1225
+
1226
+ // โ”€โ”€ Helpers โ”€โ”€
1227
+ function setResult(el, msg, cls) {
1228
+ el.textContent = msg;
1229
+ el.className = 'result-msg ' + cls;
1230
+ }
1231
+ function escHtml(s) {
1232
+ const d = document.createElement('div');
1233
+ d.textContent = s; return d.innerHTML;
1234
+ }
1235
+ function showToast(msg) {
1236
+ // simple alert fallback
1237
+ const t = document.createElement('div');
1238
+ t.textContent = msg;
1239
+ Object.assign(t.style, {
1240
+ position:'fixed', bottom:'24px', right:'24px', zIndex:999,
1241
+ background:'#1a1a2e', border:'1px solid var(--border)',
1242
+ color:'var(--text)', fontFamily:'var(--font-mono)',
1243
+ fontSize:'12px', padding:'10px 18px', borderRadius:'8px',
1244
+ boxShadow:'0 4px 24px rgba(0,0,0,0.5)',
1245
+ animation:'fadeUp 0.3s ease'
1246
+ });
1247
+ document.body.appendChild(t);
1248
+ setTimeout(() => t.remove(), 3000);
1249
+ }
1250
+
1251
+ // โ”€โ”€ Init โ”€โ”€
1252
+ checkCookie();
1253
+ refreshStatus();
1254
+ setInterval(refreshStatus, 5000);
1255
+ </script>
1256
  </body>
1257
  </html>
1258
+
1259
  '''
1260
 
1261
+ if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=False)