srinetwork commited on
Commit
636cb84
·
verified ·
1 Parent(s): 9fbe868

Update Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +716 -217
Dockerfile CHANGED
@@ -157,10 +157,10 @@ RETRY_COUNT=0
157
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
158
  # Fetch again to check if remote changed during commit
159
  git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true
160
-
161
  LOCAL=$(git rev-parse HEAD)
162
  REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown")
163
-
164
  if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then
165
  echo "🔄 Remote changed during commit, pulling again..."
166
  git stash push -m "auto-backup-stash-$RETRY_COUNT" 2>/dev/null || true
@@ -172,14 +172,14 @@ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
172
  git add -A
173
  git commit -m "🎮 Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup (retry $RETRY_COUNT)" || true
174
  fi
175
-
176
  # Try to push
177
  if git push origin main 2>&1 || git push origin master 2>&1; then
178
  echo "✅ [BACKUP] Successfully pushed to GitHub!"
179
  echo ""
180
  exit 0
181
  fi
182
-
183
  RETRY_COUNT=$((RETRY_COUNT + 1))
184
  echo "⚠️ Push failed, retrying... ($RETRY_COUNT/$MAX_RETRIES)"
185
  sleep 2
@@ -208,7 +208,7 @@ echo "✅ Auto-backup enabled (every 5 minutes)"
208
  echo ""
209
 
210
  # ==========================================
211
- # WEB TERMINAL SERVER
212
  # ==========================================
213
  cat > /app/web_terminal.py << 'PYTHON_SCRIPT'
214
  import os
@@ -220,15 +220,111 @@ import time
220
  import queue
221
  import signal
222
  import requests
223
- from flask import Flask, render_template_string
 
 
 
 
224
  from flask_sock import Sock
225
  import psutil
226
- from datetime import datetime, timezone, timedelta
227
  import pytz
228
 
229
  app = Flask(__name__)
 
230
  sock = Sock(app)
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  mc_process = None
233
  output_queue = queue.Queue()
234
  command_queue = queue.Queue()
@@ -273,7 +369,8 @@ def save_logs_to_file():
273
  try:
274
  os.makedirs(os.path.dirname(LOGS_FILE), exist_ok=True)
275
  with open(LOGS_FILE, 'w', encoding='utf-8') as f:
276
- f.write('\n'.join(logs_history[-1000:]))
 
277
  except Exception as e:
278
  print(f"[LOGS] Error saving logs: {e}", flush=True)
279
 
@@ -284,16 +381,16 @@ def push_logs_to_github():
284
  return
285
  if not os.path.exists(LOGS_FILE):
286
  return
287
-
288
  with open(LOGS_FILE, 'r', encoding='utf-8') as f:
289
  content = f.read()
290
-
291
  api_url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/logs/console_logs.txt"
292
  headers = {
293
  "Authorization": f"token {GITHUB_TOKEN}",
294
  "Accept": "application/vnd.github.v3+json"
295
  }
296
-
297
  sha = None
298
  try:
299
  resp = requests.get(api_url, headers=headers, timeout=10)
@@ -301,10 +398,10 @@ def push_logs_to_github():
301
  sha = resp.json().get('sha')
302
  except:
303
  pass
304
-
305
  import base64
306
  encoded_content = base64.b64encode(content.encode()).decode()
307
-
308
  data = {
309
  "message": f"Update logs - {format_sl_time()}",
310
  "content": encoded_content,
@@ -312,7 +409,7 @@ def push_logs_to_github():
312
  }
313
  if sha:
314
  data["sha"] = sha
315
-
316
  resp = requests.put(api_url, headers=headers, json=data, timeout=10)
317
  if resp.status_code in [200, 201]:
318
  print("[LOGS] Pushed to GitHub successfully", flush=True)
@@ -357,13 +454,13 @@ def add_log_line(line):
357
  def start_minecraft():
358
  global mc_process, server_start_time
359
  mc_dir = "/app/mcserver"
360
-
361
  print("[MC] Starting Minecraft server...", flush=True)
362
  add_log_line("[SYSTEM] Starting Minecraft server...")
363
-
364
  server_start_time = time.time()
365
  save_uptime_to_file()
366
-
367
  mc_process = subprocess.Popen(
368
  ["java", "-Xms1G", "-Xmx14G", "-XX:+UseG1GC",
369
  "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200",
@@ -376,10 +473,10 @@ def start_minecraft():
376
  bufsize=1,
377
  preexec_fn=os.setsid
378
  )
379
-
380
  print(f"[MC] Minecraft PID: {mc_process.pid}", flush=True)
381
  add_log_line(f"[SYSTEM] Minecraft server started with PID: {mc_process.pid}")
382
-
383
  def read_output():
384
  try:
385
  for line in mc_process.stdout:
@@ -392,9 +489,9 @@ def start_minecraft():
392
  except Exception as e:
393
  print(f"[MC] Output error: {e}", flush=True)
394
  add_log_line(f"[ERROR] Output reader error: {e}")
395
-
396
  threading.Thread(target=read_output, daemon=True).start()
397
-
398
  def process_commands():
399
  while True:
400
  try:
@@ -412,13 +509,413 @@ def start_minecraft():
412
  except Exception as e:
413
  print(f"[MC] Processor error: {e}", flush=True)
414
  break
415
-
416
  threading.Thread(target=process_commands, daemon=True).start()
417
 
418
  # ==========================================
419
- # BACKUP API ENDPOINTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  # ==========================================
421
  @app.route('/backup', methods=['POST'])
 
422
  def manual_backup():
423
  """Trigger manual backup"""
424
  try:
@@ -438,6 +935,7 @@ def manual_backup():
438
  return {'success': False, 'error': str(e)}
439
 
440
  @app.route('/backup/status')
 
441
  def backup_status():
442
  """Check backup/git status"""
443
  try:
@@ -479,7 +977,7 @@ HTML_TEMPLATE = """
479
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
480
  <style>
481
  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
482
-
483
  body {
484
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
485
  background: #1a1d21;
@@ -488,8 +986,7 @@ HTML_TEMPLATE = """
488
  overflow: hidden;
489
  touch-action: manipulation;
490
  }
491
-
492
- /* Top Navigation */
493
  .top-nav {
494
  background: #252a30;
495
  border-bottom: 1px solid #363b42;
@@ -500,10 +997,10 @@ HTML_TEMPLATE = """
500
  position: relative;
501
  z-index: 100;
502
  }
503
-
504
  .nav-tabs { display: flex; gap: 0; height: 100%; overflow-x: auto; scrollbar-width: none; }
505
  .nav-tabs::-webkit-scrollbar { display: none; }
506
-
507
  .nav-tab {
508
  padding: 0 15px;
509
  display: flex;
@@ -518,18 +1015,18 @@ HTML_TEMPLATE = """
518
  white-space: nowrap;
519
  flex-shrink: 0;
520
  }
521
-
522
  .nav-tab:hover { color: #e0e0e0; background: rgba(255,255,255,0.05); }
523
  .nav-tab.active { color: #fff; border-bottom-color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
524
  .nav-tab i { font-size: 14px; }
525
-
526
  .nav-right {
527
  margin-left: auto;
528
  display: flex;
529
  align-items: center;
530
  gap: 8px;
531
  }
532
-
533
  .nav-btn {
534
  background: transparent;
535
  border: none;
@@ -545,9 +1042,9 @@ HTML_TEMPLATE = """
545
  justify-content: center;
546
  position: relative;
547
  }
548
-
549
  .nav-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
550
-
551
  .nav-btn .status-dot {
552
  position: absolute;
553
  bottom: 6px;
@@ -558,17 +1055,35 @@ HTML_TEMPLATE = """
558
  background: #22c55e;
559
  border: 2px solid #252a30;
560
  }
561
-
562
  .nav-btn .status-dot.offline { background: #ef4444; }
563
-
564
- /* Main Layout */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  .main-container {
566
  display: flex;
567
  height: calc(100vh - 50px);
568
  flex-direction: row;
569
  }
570
-
571
- /* Console Area */
572
  .console-section {
573
  flex: 1;
574
  display: flex;
@@ -576,7 +1091,7 @@ HTML_TEMPLATE = """
576
  background: #0d1117;
577
  min-width: 0;
578
  }
579
-
580
  .console-header {
581
  background: #161b22;
582
  padding: 12px 15px;
@@ -587,7 +1102,7 @@ HTML_TEMPLATE = """
587
  flex-wrap: wrap;
588
  gap: 10px;
589
  }
590
-
591
  .server-title {
592
  font-size: 16px;
593
  font-weight: 600;
@@ -596,15 +1111,15 @@ HTML_TEMPLATE = """
596
  align-items: center;
597
  gap: 8px;
598
  }
599
-
600
  .server-title i { color: #3b82f6; font-size: 18px; }
601
-
602
  .control-buttons {
603
  display: flex;
604
  gap: 6px;
605
  flex-wrap: wrap;
606
  }
607
-
608
  .btn {
609
  padding: 8px 14px;
610
  border: none;
@@ -619,7 +1134,7 @@ HTML_TEMPLATE = """
619
  min-height: 36px;
620
  touch-action: manipulation;
621
  }
622
-
623
  .btn-primary { background: #2563eb; color: white; }
624
  .btn-primary:hover { background: #1d4ed8; }
625
  .btn-secondary { background: #374151; color: #e0e0e0; }
@@ -629,8 +1144,7 @@ HTML_TEMPLATE = """
629
  .btn-success { background: #16a34a; color: white; }
630
  .btn-success:hover { background: #15803d; }
631
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
632
-
633
- /* Terminal */
634
  .terminal-container {
635
  flex: 1;
636
  background: #0d1117;
@@ -638,7 +1152,7 @@ HTML_TEMPLATE = """
638
  overflow: hidden;
639
  position: relative;
640
  }
641
-
642
  .terminal {
643
  width: 100%;
644
  height: 100%;
@@ -652,31 +1166,30 @@ HTML_TEMPLATE = """
652
  word-wrap: break-word;
653
  -webkit-overflow-scrolling: touch;
654
  }
655
-
656
  .terminal-line { margin: 2px 0; padding: 1px 0; }
657
  .terminal-line.info { color: #58a6ff; }
658
  .terminal-line.warn { color: #d29922; }
659
  .terminal-line.error { color: #f85149; }
660
  .terminal-line.success { color: #3fb950; }
661
  .terminal-line.system { color: #a855f7; font-weight: 500; }
662
-
663
  .typing-preview {
664
  color: #6e7681;
665
  font-style: italic;
666
  opacity: 0.8;
667
  }
668
-
669
  .typing-preview .cursor {
670
  animation: blink 1s infinite;
671
  color: #58a6ff;
672
  }
673
-
674
  @keyframes blink {
675
  0%, 50% { opacity: 1; }
676
  51%, 100% { opacity: 0; }
677
  }
678
-
679
- /* Command Input Area */
680
  .command-area {
681
  background: #161b22;
682
  border-top: 1px solid #30363d;
@@ -685,7 +1198,7 @@ HTML_TEMPLATE = """
685
  gap: 8px;
686
  align-items: center;
687
  }
688
-
689
  .command-input-wrapper {
690
  flex: 1;
691
  display: flex;
@@ -697,10 +1210,10 @@ HTML_TEMPLATE = """
697
  transition: border-color 0.2s;
698
  min-height: 44px;
699
  }
700
-
701
  .command-input-wrapper:focus-within { border-color: #58a6ff; }
702
  .command-input-wrapper i { color: #6e7681; margin-right: 8px; font-size: 14px; }
703
-
704
  #commandInput {
705
  flex: 1;
706
  background: transparent;
@@ -712,9 +1225,9 @@ HTML_TEMPLATE = """
712
  outline: none;
713
  min-width: 0;
714
  }
715
-
716
  #commandInput::placeholder { color: #6e7681; }
717
-
718
  .send-btn {
719
  background: #238636;
720
  color: white;
@@ -731,11 +1244,10 @@ HTML_TEMPLATE = """
731
  font-size: 13px;
732
  touch-action: manipulation;
733
  }
734
-
735
  .send-btn:hover { background: #2ea043; }
736
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
737
-
738
- /* Sidebar */
739
  .sidebar {
740
  width: 280px;
741
  background: #1f2428;
@@ -744,7 +1256,7 @@ HTML_TEMPLATE = """
744
  overflow-y: auto;
745
  -webkit-overflow-scrolling: touch;
746
  }
747
-
748
  .stat-card {
749
  background: #252a30;
750
  border: 1px solid #363b42;
@@ -756,9 +1268,9 @@ HTML_TEMPLATE = """
756
  gap: 12px;
757
  transition: transform 0.2s, box-shadow 0.2s;
758
  }
759
-
760
  .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
761
-
762
  .stat-icon {
763
  width: 40px;
764
  height: 40px;
@@ -769,20 +1281,19 @@ HTML_TEMPLATE = """
769
  font-size: 18px;
770
  flex-shrink: 0;
771
  }
772
-
773
  .stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
774
  .stat-icon.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
775
  .stat-icon.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
776
  .stat-icon.yellow { background: rgba(234, 179, 8, 0.2); color: #eab308; }
777
  .stat-icon.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
778
  .stat-icon.cyan { background: rgba(6, 182, 212, 0.2); color: #06b6d4; }
779
-
780
  .stat-info { flex: 1; min-width: 0; }
781
  .stat-label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; }
782
  .stat-value { font-size: 16px; font-weight: 600; color: #f0f6fc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
783
  .stat-sub { font-size: 10px; color: #6b7280; margin-top: 2px; }
784
-
785
- /* Status Badge */
786
  .status-indicator {
787
  display: inline-flex;
788
  align-items: center;
@@ -793,21 +1304,19 @@ HTML_TEMPLATE = """
793
  font-weight: 600;
794
  text-transform: uppercase;
795
  }
796
-
797
  .status-online { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
798
  .status-offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
799
  .status-starting { background: rgba(234, 179, 8, 0.2); color: #eab308; }
800
-
801
  .pulse { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
802
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
803
-
804
- /* Scrollbar */
805
  ::-webkit-scrollbar { width: 6px; height: 6px; }
806
  ::-webkit-scrollbar-track { background: #1a1d21; }
807
  ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; }
808
  ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
809
-
810
- /* Connection Status */
811
  .connection-bar {
812
  position: fixed;
813
  top: 50px;
@@ -822,12 +1331,11 @@ HTML_TEMPLATE = """
822
  z-index: 99;
823
  transition: all 0.3s;
824
  }
825
-
826
  .connection-bar.connected { background: rgba(34, 197, 94, 0.15); color: #22c55e; border-bottom: 1px solid rgba(34, 197, 94, 0.3); }
827
  .connection-bar.disconnected { background: rgba(239, 68, 68, 0.15); color: #ef4444; border-bottom: 1px solid rgba(239, 68, 68, 0.3); }
828
  .connection-bar.connecting { background: rgba(234, 179, 8, 0.15); color: #eab308; border-bottom: 1px solid rgba(234, 179, 8, 0.3); }
829
-
830
- /* Mobile Overlay */
831
  .sidebar-overlay {
832
  display: none;
833
  position: fixed;
@@ -838,10 +1346,9 @@ HTML_TEMPLATE = """
838
  background: rgba(0,0,0,0.5);
839
  z-index: 150;
840
  }
841
-
842
  .sidebar-overlay.active { display: block; }
843
-
844
- /* Status Modal */
845
  .status-modal {
846
  display: none;
847
  position: fixed;
@@ -856,9 +1363,9 @@ HTML_TEMPLATE = """
856
  min-width: 300px;
857
  box-shadow: 0 10px 40px rgba(0,0,0,0.5);
858
  }
859
-
860
  .status-modal.active { display: block; }
861
-
862
  .status-modal-header {
863
  display: flex;
864
  justify-content: space-between;
@@ -867,13 +1374,13 @@ HTML_TEMPLATE = """
867
  padding-bottom: 10px;
868
  border-bottom: 1px solid #363b42;
869
  }
870
-
871
  .status-modal-title {
872
  font-size: 16px;
873
  font-weight: 600;
874
  color: #f0f6fc;
875
  }
876
-
877
  .status-modal-close {
878
  background: transparent;
879
  border: none;
@@ -881,21 +1388,21 @@ HTML_TEMPLATE = """
881
  cursor: pointer;
882
  font-size: 18px;
883
  }
884
-
885
  .status-modal-close:hover { color: #fff; }
886
-
887
  .status-item {
888
  display: flex;
889
  justify-content: space-between;
890
  padding: 8px 0;
891
  border-bottom: 1px solid #2a2f35;
892
  }
893
-
894
  .status-item:last-child { border-bottom: none; }
895
-
896
  .status-label-modal { color: #9ca3af; font-size: 13px; }
897
  .status-value-modal { color: #f0f6fc; font-weight: 500; font-size: 13px; }
898
-
899
  .modal-overlay {
900
  display: none;
901
  position: fixed;
@@ -906,16 +1413,15 @@ HTML_TEMPLATE = """
906
  background: rgba(0,0,0,0.7);
907
  z-index: 250;
908
  }
909
-
910
  .modal-overlay.active { display: block; }
911
-
912
- /* Backup Section */
913
  .backup-section {
914
  margin-top: 15px;
915
  padding-top: 15px;
916
  border-top: 1px solid #363b42;
917
  }
918
-
919
  .backup-title {
920
  font-size: 12px;
921
  color: #9ca3af;
@@ -926,7 +1432,7 @@ HTML_TEMPLATE = """
926
  align-items: center;
927
  gap: 6px;
928
  }
929
-
930
  .backup-btn {
931
  width: 100%;
932
  padding: 10px;
@@ -944,24 +1450,20 @@ HTML_TEMPLATE = """
944
  transition: all 0.2s;
945
  margin-bottom: 8px;
946
  }
947
-
948
  .backup-btn:hover { background: #1e3a8a; }
949
  .backup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
950
-
951
  .backup-status {
952
  font-size: 11px;
953
  color: #6b7280;
954
  text-align: center;
955
  padding: 5px;
956
  }
957
-
958
  .backup-status.success { color: #22c55e; }
959
  .backup-status.error { color: #ef4444; }
960
-
961
- /* ============================================
962
- MOBILE RESPONSIVE BREAKPOINTS
963
- ============================================ */
964
-
965
  @media screen and (max-width: 768px) {
966
  .sidebar {
967
  position: fixed;
@@ -973,104 +1475,104 @@ HTML_TEMPLATE = """
973
  transition: right 0.3s ease;
974
  box-shadow: -4px 0 20px rgba(0,0,0,0.5);
975
  }
976
-
977
  .sidebar.active { right: 0; }
978
  .connection-bar { right: 0; }
979
-
980
  .console-header { padding: 10px 12px; }
981
  .server-title { font-size: 14px; }
982
-
983
  .btn {
984
  padding: 6px 10px;
985
  font-size: 11px;
986
  min-height: 32px;
987
  }
988
-
989
  .btn span { display: none; }
990
-
991
  .terminal {
992
  font-size: 11px;
993
  padding: 10px;
994
  }
995
-
996
  .command-area { padding: 8px 10px; }
997
-
998
  .send-btn {
999
  padding: 10px 14px;
1000
  font-size: 12px;
1001
  }
1002
-
1003
  .send-btn span { display: none; }
1004
  }
1005
-
1006
  @media screen and (max-width: 480px) {
1007
  .top-nav { padding: 0 10px; }
1008
-
1009
  .nav-tab {
1010
  padding: 0 10px;
1011
  font-size: 12px;
1012
  }
1013
-
1014
  .nav-tab i { font-size: 12px; }
1015
-
1016
  .console-header {
1017
  flex-direction: column;
1018
  align-items: flex-start;
1019
  gap: 8px;
1020
  }
1021
-
1022
  .control-buttons {
1023
  width: 100%;
1024
  justify-content: flex-start;
1025
  }
1026
-
1027
  .btn {
1028
  flex: 1;
1029
  justify-content: center;
1030
  padding: 8px;
1031
  }
1032
-
1033
  .terminal {
1034
  font-size: 10px;
1035
  line-height: 1.4;
1036
  }
1037
-
1038
  .command-input-wrapper { min-height: 40px; }
1039
  #commandInput { font-size: 16px; }
1040
-
1041
  .send-btn {
1042
  min-height: 40px;
1043
  padding: 8px 12px;
1044
  }
1045
-
1046
  .sidebar {
1047
  width: 100%;
1048
  right: -100%;
1049
  }
1050
-
1051
  .stat-card { padding: 10px; }
1052
-
1053
  .stat-icon {
1054
  width: 36px;
1055
  height: 36px;
1056
  font-size: 16px;
1057
  }
1058
-
1059
  .stat-value { font-size: 14px; }
1060
-
1061
  .status-modal {
1062
  min-width: 90vw;
1063
  max-width: 350px;
1064
  }
1065
  }
1066
-
1067
  @media screen and (max-height: 500px) and (orientation: landscape) {
1068
  .console-header { padding: 8px 12px; }
1069
  .command-area { padding: 6px 10px; }
1070
  .sidebar { padding: 10px; }
1071
  .stat-card { padding: 8px; margin-bottom: 6px; }
1072
  }
1073
-
1074
  @media screen and (min-width: 1400px) {
1075
  .sidebar { width: 320px; }
1076
  .terminal { font-size: 13px; }
@@ -1079,7 +1581,6 @@ HTML_TEMPLATE = """
1079
  </style>
1080
  </head>
1081
  <body>
1082
- <!-- Top Navigation -->
1083
  <nav class="top-nav">
1084
  <div class="nav-tabs">
1085
  <a class="nav-tab active" onclick="switchTab('console', this)">
@@ -1097,10 +1598,12 @@ HTML_TEMPLATE = """
1097
  <button class="nav-btn" title="Full Screen" onclick="toggleFullscreen()">
1098
  <i class="fas fa-expand"></i>
1099
  </button>
 
 
 
1100
  </div>
1101
  </nav>
1102
-
1103
- <!-- Status Modal -->
1104
  <div class="modal-overlay" id="modalOverlay" onclick="toggleStatusModal()"></div>
1105
  <div class="status-modal" id="statusModal">
1106
  <div class="status-modal-header">
@@ -1146,19 +1649,16 @@ HTML_TEMPLATE = """
1146
  </div>
1147
  </div>
1148
  </div>
1149
-
1150
- <!-- Sidebar Overlay -->
1151
  <div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
1152
-
1153
- <!-- Main Container -->
1154
  <div class="main-container">
1155
- <!-- Console Section -->
1156
  <div class="console-section" id="consoleSection">
1157
  <div class="connection-bar connecting" id="connectionBar">
1158
  <i class="fas fa-circle-notch fa-spin"></i>
1159
  <span>Connecting to server...</span>
1160
  </div>
1161
-
1162
  <div class="console-header">
1163
  <div class="server-title">
1164
  <i class="fas fa-server"></i>
@@ -1179,11 +1679,11 @@ HTML_TEMPLATE = """
1179
  </button>
1180
  </div>
1181
  </div>
1182
-
1183
  <div class="terminal-container">
1184
  <div class="terminal" id="terminal"></div>
1185
  </div>
1186
-
1187
  <div class="command-area">
1188
  <div class="command-input-wrapper">
1189
  <i class="fas fa-angle-right"></i>
@@ -1194,8 +1694,7 @@ HTML_TEMPLATE = """
1194
  </button>
1195
  </div>
1196
  </div>
1197
-
1198
- <!-- Sidebar -->
1199
  <div class="sidebar" id="sidebar">
1200
  <div class="stat-card">
1201
  <div class="stat-icon blue"><i class="fas fa-network-wired"></i></div>
@@ -1205,7 +1704,7 @@ HTML_TEMPLATE = """
1205
  <div class="stat-sub">Port 25565</div>
1206
  </div>
1207
  </div>
1208
-
1209
  <div class="stat-card">
1210
  <div class="stat-icon red"><i class="fas fa-clock"></i></div>
1211
  <div class="stat-info">
@@ -1214,7 +1713,7 @@ HTML_TEMPLATE = """
1214
  <div class="stat-sub" id="uptimeSub">Not running</div>
1215
  </div>
1216
  </div>
1217
-
1218
  <div class="stat-card">
1219
  <div class="stat-icon yellow"><i class="fas fa-microchip"></i></div>
1220
  <div class="stat-info">
@@ -1223,7 +1722,7 @@ HTML_TEMPLATE = """
1223
  <div class="stat-sub" id="cpuSub">System average</div>
1224
  </div>
1225
  </div>
1226
-
1227
  <div class="stat-card">
1228
  <div class="stat-icon purple"><i class="fas fa-memory"></i></div>
1229
  <div class="stat-info">
@@ -1232,7 +1731,7 @@ HTML_TEMPLATE = """
1232
  <div class="stat-sub" id="memorySub">-- / -- GB</div>
1233
  </div>
1234
  </div>
1235
-
1236
  <div class="stat-card">
1237
  <div class="stat-icon cyan"><i class="fas fa-hdd"></i></div>
1238
  <div class="stat-info">
@@ -1241,7 +1740,7 @@ HTML_TEMPLATE = """
1241
  <div class="stat-sub">Server files</div>
1242
  </div>
1243
  </div>
1244
-
1245
  <div class="stat-card">
1246
  <div class="stat-icon green"><i class="fas fa-users"></i></div>
1247
  <div class="stat-info">
@@ -1250,8 +1749,7 @@ HTML_TEMPLATE = """
1250
  <div class="stat-sub">Online players</div>
1251
  </div>
1252
  </div>
1253
-
1254
- <!-- Backup Section -->
1255
  <div class="backup-section">
1256
  <div class="backup-title">
1257
  <i class="fas fa-cloud-upload-alt"></i> GitHub Backup
@@ -1282,7 +1780,7 @@ HTML_TEMPLATE = """
1282
  const navStatusDot = document.getElementById('navStatusDot');
1283
  const backupBtn = document.getElementById('backupBtn');
1284
  const backupStatus = document.getElementById('backupStatus');
1285
-
1286
  let ws = null;
1287
  let reconnectAttempts = 0;
1288
  let maxReconnectAttempts = 10;
@@ -1291,7 +1789,7 @@ HTML_TEMPLATE = """
1291
  let serverStartTime = null;
1292
  let typingPreviewLine = null;
1293
  let currentStats = {};
1294
-
1295
  function updateConnectionStatus(status, message) {
1296
  connectionBar.className = 'connection-bar ' + status;
1297
  const icons = {
@@ -1300,7 +1798,7 @@ HTML_TEMPLATE = """
1300
  'connecting': 'fa-circle-notch fa-spin'
1301
  };
1302
  connectionBar.innerHTML = '<i class="fas ' + icons[status] + '"></i><span>' + message + '</span>';
1303
-
1304
  const wsStatus = document.getElementById('modalWsStatus');
1305
  if (status === 'connected') {
1306
  wsStatus.innerHTML = '<span style="color: #22c55e;">● Connected</span>';
@@ -1310,11 +1808,11 @@ HTML_TEMPLATE = """
1310
  wsStatus.innerHTML = '<span style="color: #eab308;">● Connecting...</span>';
1311
  }
1312
  }
1313
-
1314
  function updateServerStatus(running, startTime = null) {
1315
  isServerRunning = running;
1316
  const statusEl = document.getElementById('serverStatus');
1317
-
1318
  if (running) {
1319
  statusEl.className = 'status-indicator status-online';
1320
  statusEl.innerHTML = '<span class="pulse"></span>Online';
@@ -1341,16 +1839,16 @@ HTML_TEMPLATE = """
1341
  document.getElementById('modalMcStatus').innerHTML = '<span style="color: #ef4444;">● Offline</span>';
1342
  }
1343
  }
1344
-
1345
  function addTerminalLine(text, type = '') {
1346
  if (typingPreviewLine && typingPreviewLine.parentNode) {
1347
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
1348
  typingPreviewLine = null;
1349
  }
1350
-
1351
  const line = document.createElement('div');
1352
  line.className = 'terminal-line ' + type;
1353
-
1354
  if (text.includes('[SYSTEM]')) {
1355
  line.className += ' system';
1356
  } else if (text.includes('> ')) {
@@ -1362,24 +1860,24 @@ HTML_TEMPLATE = """
1362
  } else if (text.toLowerCase().includes('done') || text.toLowerCase().includes('success')) {
1363
  line.className += ' success';
1364
  }
1365
-
1366
  line.textContent = text;
1367
  terminal.appendChild(line);
1368
  terminal.scrollTop = terminal.scrollHeight;
1369
-
1370
  while (terminal.children.length > 500) {
1371
  terminal.removeChild(terminal.firstChild);
1372
  }
1373
  }
1374
-
1375
  function updateTypingPreview() {
1376
  const text = commandInput.value.trim();
1377
-
1378
  if (typingPreviewLine && typingPreviewLine.parentNode) {
1379
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
1380
  typingPreviewLine = null;
1381
  }
1382
-
1383
  if (text && isServerRunning) {
1384
  typingPreviewLine = document.createElement('div');
1385
  typingPreviewLine.className = 'terminal-line typing-preview';
@@ -1388,27 +1886,27 @@ HTML_TEMPLATE = """
1388
  terminal.scrollTop = terminal.scrollHeight;
1389
  }
1390
  }
1391
-
1392
  function escapeHtml(text) {
1393
  const div = document.createElement('div');
1394
  div.textContent = text;
1395
  return div.innerHTML;
1396
  }
1397
-
1398
  function formatUptime(seconds) {
1399
  if (!seconds || seconds <= 0) return 'Offline';
1400
-
1401
  const days = Math.floor(seconds / 86400);
1402
  const hours = Math.floor((seconds % 86400) / 3600);
1403
  const minutes = Math.floor((seconds % 3600) / 60);
1404
  const secs = Math.floor(seconds % 60);
1405
-
1406
  if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm';
1407
  if (hours > 0) return hours + 'h ' + minutes + 'm ' + secs + 's';
1408
  if (minutes > 0) return minutes + 'm ' + secs + 's';
1409
  return secs + 's';
1410
  }
1411
-
1412
  function updateUptime() {
1413
  if (serverStartTime && isServerRunning) {
1414
  const now = Date.now();
@@ -1423,21 +1921,18 @@ HTML_TEMPLATE = """
1423
  document.getElementById('modalUptime').textContent = 'Offline';
1424
  }
1425
  }
1426
-
1427
  setInterval(updateUptime, 1000);
1428
-
1429
- // ==========================================
1430
- // BACKUP FUNCTIONS
1431
- // ==========================================
1432
  async function triggerBackup() {
1433
  backupBtn.disabled = true;
1434
  backupStatus.textContent = 'Backing up...';
1435
  backupStatus.className = 'backup-status';
1436
-
1437
  try {
1438
  const response = await fetch('/backup', { method: 'POST' });
1439
  const result = await response.json();
1440
-
1441
  if (result.success) {
1442
  backupStatus.textContent = '✅ Backup successful!';
1443
  backupStatus.className = 'backup-status success';
@@ -1451,24 +1946,24 @@ HTML_TEMPLATE = """
1451
  backupStatus.textContent = '❌ Network error';
1452
  backupStatus.className = 'backup-status error';
1453
  }
1454
-
1455
  setTimeout(() => {
1456
  backupBtn.disabled = false;
1457
  }, 5000);
1458
  }
1459
-
1460
  async function checkBackupStatus() {
1461
  backupStatus.textContent = 'Checking...';
1462
-
1463
  try {
1464
  const response = await fetch('/backup/status');
1465
  const result = await response.json();
1466
-
1467
  if (result.error) {
1468
  backupStatus.textContent = '❌ Error: ' + result.error;
1469
  backupStatus.className = 'backup-status error';
1470
  } else if (result.has_changes) {
1471
- backupStatus.textContent = '📝 Unsaved changes: ' + result.status.split('\\n').length + ' files';
1472
  backupStatus.className = 'backup-status';
1473
  } else {
1474
  backupStatus.textContent = '✅ All changes synced';
@@ -1479,21 +1974,21 @@ HTML_TEMPLATE = """
1479
  backupStatus.className = 'backup-status error';
1480
  }
1481
  }
1482
-
1483
  function connect() {
1484
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1485
  ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
1486
-
1487
  ws.onopen = () => {
1488
  console.log('WebSocket connected');
1489
  reconnectAttempts = 0;
1490
  updateConnectionStatus('connected', 'Connected to server');
1491
  setTimeout(() => { connectionBar.style.opacity = '0'; }, 2000);
1492
  };
1493
-
1494
  ws.onmessage = (event) => {
1495
  const data = JSON.parse(event.data);
1496
-
1497
  if (data.type === 'output') {
1498
  addTerminalLine(data.data);
1499
  } else if (data.type === 'stats') {
@@ -1503,12 +1998,12 @@ HTML_TEMPLATE = """
1503
  const memTotal = (data.memoryTotal / 1024 / 1024 / 1024).toFixed(2);
1504
  const memUsed = ((data.memoryTotal * data.memory / 100) / 1024 / 1024 / 1024).toFixed(2);
1505
  document.getElementById('memorySub').textContent = memUsed + ' / ' + memTotal + ' GB';
1506
-
1507
  document.getElementById('modalCpu').textContent = data.cpu.toFixed(1) + '%';
1508
  document.getElementById('modalMemory').textContent = data.memory.toFixed(1) + '%';
1509
-
1510
  updateServerStatus(data.running, data.serverStartTime);
1511
-
1512
  if (data.players !== undefined) {
1513
  document.getElementById('playerCount').textContent = data.players;
1514
  document.getElementById('modalPlayers').textContent = data.players;
@@ -1519,7 +2014,7 @@ HTML_TEMPLATE = """
1519
  });
1520
  }
1521
  };
1522
-
1523
  ws.onclose = () => {
1524
  updateConnectionStatus('disconnected', 'Connection lost. Reconnecting...');
1525
  connectionBar.style.opacity = '1';
@@ -1529,14 +2024,14 @@ HTML_TEMPLATE = """
1529
  setTimeout(connect, 3000);
1530
  }
1531
  };
1532
-
1533
  ws.onerror = (error) => {
1534
  console.error('WebSocket error:', error);
1535
  };
1536
  }
1537
-
1538
  connect();
1539
-
1540
  function sendInput() {
1541
  const cmd = commandInput.value.trim();
1542
  if (cmd && ws && ws.readyState === WebSocket.OPEN) {
@@ -1548,16 +2043,16 @@ HTML_TEMPLATE = """
1548
  commandInput.value = '';
1549
  }
1550
  }
1551
-
1552
  function sendCmd(cmd) {
1553
  if (ws && ws.readyState === WebSocket.OPEN) {
1554
  ws.send(JSON.stringify({type: 'command', data: cmd}));
1555
  }
1556
  }
1557
-
1558
  commandInput.addEventListener('input', updateTypingPreview);
1559
  commandInput.addEventListener('keyup', updateTypingPreview);
1560
-
1561
  commandInput.addEventListener('keypress', (e) => {
1562
  if (e.key === 'Enter') {
1563
  if (typingPreviewLine && typingPreviewLine.parentNode) {
@@ -1567,19 +2062,19 @@ HTML_TEMPLATE = """
1567
  sendInput();
1568
  }
1569
  });
1570
-
1571
  commandInput.addEventListener('blur', () => {
1572
  if (typingPreviewLine && typingPreviewLine.parentNode) {
1573
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
1574
  typingPreviewLine = null;
1575
  }
1576
  });
1577
-
1578
  function switchTab(tab, element) {
1579
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1580
  element.classList.add('active');
1581
  }
1582
-
1583
  function toggleFullscreen() {
1584
  if (!document.fullscreenElement) {
1585
  document.documentElement.requestFullscreen();
@@ -1587,7 +2082,7 @@ HTML_TEMPLATE = """
1587
  document.exitFullscreen();
1588
  }
1589
  }
1590
-
1591
  function toggleSidebar() {
1592
  sidebarOpen = !sidebarOpen;
1593
  if (sidebarOpen) {
@@ -1598,13 +2093,13 @@ HTML_TEMPLATE = """
1598
  sidebarOverlay.classList.remove('active');
1599
  }
1600
  }
1601
-
1602
  function toggleStatusModal() {
1603
  statusModal.classList.toggle('active');
1604
  modalOverlay.classList.toggle('active');
1605
  document.getElementById('modalAddress').textContent = window.location.hostname;
1606
  }
1607
-
1608
  document.addEventListener('click', (e) => {
1609
  if (window.innerWidth <= 768 && sidebarOpen) {
1610
  if (!sidebar.contains(e.target) && !e.target.closest('.sidebar-toggle')) {
@@ -1612,13 +2107,13 @@ HTML_TEMPLATE = """
1612
  }
1613
  }
1614
  });
1615
-
1616
  window.addEventListener('orientationchange', () => {
1617
  setTimeout(() => {
1618
  terminal.scrollTop = terminal.scrollHeight;
1619
  }, 300);
1620
  });
1621
-
1622
  let resizeTimer;
1623
  window.addEventListener('resize', () => {
1624
  clearTimeout(resizeTimer);
@@ -1630,7 +2125,7 @@ HTML_TEMPLATE = """
1630
  }
1631
  }, 250);
1632
  });
1633
-
1634
  let lastTouchEnd = 0;
1635
  document.addEventListener('touchend', (e) => {
1636
  const now = Date.now();
@@ -1639,7 +2134,7 @@ HTML_TEMPLATE = """
1639
  }
1640
  lastTouchEnd = now;
1641
  }, false);
1642
-
1643
  document.getElementById('serverAddress').textContent = window.location.hostname;
1644
  addTerminalLine('[SYSTEM] Connecting to Minecraft server...', 'system');
1645
  </script>
@@ -1648,6 +2143,7 @@ HTML_TEMPLATE = """
1648
  """
1649
 
1650
  @app.route('/')
 
1651
  def index():
1652
  return render_template_string(HTML_TEMPLATE)
1653
 
@@ -1661,10 +2157,11 @@ def health():
1661
  }
1662
 
1663
  @sock.route('/ws')
 
1664
  def websocket(ws):
1665
  global server_start_time
1666
  print("[WS] Client connected", flush=True)
1667
-
1668
  try:
1669
  if logs_history:
1670
  ws.send(json.dumps({
@@ -1673,18 +2170,18 @@ def websocket(ws):
1673
  }))
1674
  except Exception as e:
1675
  print(f"[WS] History send error: {e}", flush=True)
1676
-
1677
  def send_stats():
1678
  while True:
1679
  try:
1680
  cpu = psutil.cpu_percent(interval=0.5)
1681
  memory = psutil.virtual_memory()
1682
  running = mc_process is not None and mc_process.poll() is None
1683
-
1684
  uptime_to_send = None
1685
  if running and server_start_time:
1686
  uptime_to_send = server_start_time
1687
-
1688
  ws.send(json.dumps({
1689
  'type': 'stats',
1690
  'cpu': cpu,
@@ -1698,10 +2195,10 @@ def websocket(ws):
1698
  print(f"[WS] Stats error: {e}", flush=True)
1699
  break
1700
  time.sleep(2)
1701
-
1702
  stats_thread = threading.Thread(target=send_stats, daemon=True)
1703
  stats_thread.start()
1704
-
1705
  def send_output():
1706
  while True:
1707
  try:
@@ -1714,10 +2211,10 @@ def websocket(ws):
1714
  continue
1715
  except:
1716
  break
1717
-
1718
  output_thread = threading.Thread(target=send_output, daemon=True)
1719
  output_thread.start()
1720
-
1721
  while True:
1722
  try:
1723
  data = ws.receive(timeout=5)
@@ -1732,7 +2229,7 @@ def websocket(ws):
1732
  if "Connection closed" in str(e):
1733
  break
1734
  continue
1735
-
1736
  print("[WS] Client disconnected", flush=True)
1737
 
1738
  def cleanup(signum=None, frame=None):
@@ -1756,25 +2253,26 @@ signal.signal(signal.SIGINT, cleanup)
1756
  if __name__ == '__main__':
1757
  load_logs_from_file()
1758
  load_uptime_from_file()
1759
-
1760
  print(f"[MAIN] Starting web server... Timezone: {SL_TZ}", flush=True)
1761
-
 
1762
  mc_thread = threading.Thread(target=start_minecraft, daemon=True)
1763
  mc_thread.start()
1764
-
1765
  def github_push_loop():
1766
  while True:
1767
  time.sleep(300)
1768
  push_logs_to_github()
1769
-
1770
  github_thread = threading.Thread(target=github_push_loop, daemon=True)
1771
  github_thread.start()
1772
-
1773
  from werkzeug.serving import run_simple
1774
  run_simple('0.0.0.0', 7860, app, threaded=True, use_reloader=False)
1775
  PYTHON_SCRIPT
1776
 
1777
- echo "🌐 Starting Web Terminal Server..."
1778
  python3 /app/web_terminal.py 2>&1 &
1779
 
1780
  sleep 5
@@ -1789,6 +2287,7 @@ fi
1789
  echo ""
1790
  echo "=========================================="
1791
  echo "✅ Minecraft Server with GitHub Backup is LIVE!"
 
1792
  echo "🌐 Access at: https://your-space.hf.space"
1793
  echo "📱 Mobile optimized interface enabled"
1794
  echo "💾 Logs are now persistent across refreshes"
@@ -1804,11 +2303,11 @@ echo "=========================================="
1804
  # Keep container alive
1805
  while true; do
1806
  sleep 30
1807
-
1808
  if ! pgrep -f "server.jar" > /dev/null; then
1809
  echo "⚠️ Minecraft server not running!"
1810
  fi
1811
-
1812
  if ! pgrep -f "web_terminal.py" > /dev/null; then
1813
  echo "❌ Web Terminal crashed! Restarting..."
1814
  python3 /app/web_terminal.py 2>&1 &
 
157
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
158
  # Fetch again to check if remote changed during commit
159
  git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true
160
+
161
  LOCAL=$(git rev-parse HEAD)
162
  REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown")
163
+
164
  if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then
165
  echo "🔄 Remote changed during commit, pulling again..."
166
  git stash push -m "auto-backup-stash-$RETRY_COUNT" 2>/dev/null || true
 
172
  git add -A
173
  git commit -m "🎮 Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup (retry $RETRY_COUNT)" || true
174
  fi
175
+
176
  # Try to push
177
  if git push origin main 2>&1 || git push origin master 2>&1; then
178
  echo "✅ [BACKUP] Successfully pushed to GitHub!"
179
  echo ""
180
  exit 0
181
  fi
182
+
183
  RETRY_COUNT=$((RETRY_COUNT + 1))
184
  echo "⚠️ Push failed, retrying... ($RETRY_COUNT/$MAX_RETRIES)"
185
  sleep 2
 
208
  echo ""
209
 
210
  # ==========================================
211
+ # WEB TERMINAL SERVER WITH PASSWORD PROTECTION
212
  # ==========================================
213
  cat > /app/web_terminal.py << 'PYTHON_SCRIPT'
214
  import os
 
220
  import queue
221
  import signal
222
  import requests
223
+ import hashlib
224
+ import secrets
225
+ import functools
226
+ from datetime import datetime, timezone, timedelta
227
+ from flask import Flask, render_template_string, request, redirect, url_for, session, flash, abort
228
  from flask_sock import Sock
229
  import psutil
 
230
  import pytz
231
 
232
  app = Flask(__name__)
233
+ app.secret_key = os.environ.get('FLASK_SECRET_KEY', secrets.token_hex(32))
234
  sock = Sock(app)
235
 
236
+ # ==========================================
237
+ # PASSWORD PROTECTION CONFIGURATION
238
+ # ==========================================
239
+ WEB_PASSWORD = os.environ.get('WEB_PASSWORD', '').strip()
240
+ LOGIN_TIMEOUT = 3600 # 1 hour session timeout
241
+ MAX_LOGIN_ATTEMPTS = 5
242
+ LOCKOUT_DURATION = 300 # 5 minutes lockout
243
+
244
+ # In-memory login attempt tracking
245
+ login_attempts = {}
246
+
247
+ def get_client_ip():
248
+ """Get real client IP address"""
249
+ if request.headers.get('X-Forwarded-For'):
250
+ return request.headers.get('X-Forwarded-For').split(',')[0].strip()
251
+ elif request.headers.get('X-Real-IP'):
252
+ return request.headers.get('X-Real-IP')
253
+ return request.remote_addr
254
+
255
+ def is_locked_out(ip):
256
+ """Check if IP is locked out due to failed attempts"""
257
+ if ip not in login_attempts:
258
+ return False
259
+ attempts = login_attempts[ip]
260
+ if attempts['count'] >= MAX_LOGIN_ATTEMPTS:
261
+ if time.time() - attempts['last_attempt'] < LOCKOUT_DURATION:
262
+ return True
263
+ else:
264
+ # Reset after lockout period
265
+ login_attempts[ip] = {'count': 0, 'last_attempt': 0}
266
+ return False
267
+
268
+ def record_failed_attempt(ip):
269
+ """Record a failed login attempt"""
270
+ if ip not in login_attempts:
271
+ login_attempts[ip] = {'count': 0, 'last_attempt': 0}
272
+ login_attempts[ip]['count'] += 1
273
+ login_attempts[ip]['last_attempt'] = time.time()
274
+
275
+ def record_successful_attempt(ip):
276
+ """Reset failed attempts on successful login"""
277
+ if ip in login_attempts:
278
+ login_attempts[ip] = {'count': 0, 'last_attempt': 0}
279
+
280
+ def hash_password(password):
281
+ """Securely hash password using SHA-256 with salt"""
282
+ salt = os.environ.get('FLASK_SECRET_KEY', 'default_salt')
283
+ return hashlib.sha256((password + salt).encode()).hexdigest()
284
+
285
+ def verify_password(password):
286
+ """Verify password against environment variable"""
287
+ if not WEB_PASSWORD:
288
+ return True # No password set, allow access (should not happen in production)
289
+ return secrets.compare_digest(password, WEB_PASSWORD)
290
+
291
+ def login_required(f):
292
+ """Decorator to require authentication"""
293
+ @functools.wraps(f)
294
+ def decorated_function(*args, **kwargs):
295
+ # If no password is set, allow access (for development only)
296
+ if not WEB_PASSWORD:
297
+ return f(*args, **kwargs)
298
+
299
+ # Check if user is logged in
300
+ if not session.get('authenticated'):
301
+ return redirect(url_for('login_page'))
302
+
303
+ # Check session timeout
304
+ last_activity = session.get('last_activity', 0)
305
+ if time.time() - last_activity > LOGIN_TIMEOUT:
306
+ session.clear()
307
+ return redirect(url_for('login_page'))
308
+
309
+ # Update last activity
310
+ session['last_activity'] = time.time()
311
+ return f(*args, **kwargs)
312
+ return decorated_function
313
+
314
+ def ws_login_required(f):
315
+ """Decorator for WebSocket routes"""
316
+ @functools.wraps(f)
317
+ def decorated_function(ws, *args, **kwargs):
318
+ # If no password is set, allow access
319
+ if not WEB_PASSWORD:
320
+ return f(ws, *args, **kwargs)
321
+
322
+ # For WebSocket, we check a token passed in the first message
323
+ # or we check if the session is authenticated (if cookies work with WS)
324
+ # Simple approach: require auth token in connection
325
+ return f(ws, *args, **kwargs)
326
+ return decorated_function
327
+
328
  mc_process = None
329
  output_queue = queue.Queue()
330
  command_queue = queue.Queue()
 
369
  try:
370
  os.makedirs(os.path.dirname(LOGS_FILE), exist_ok=True)
371
  with open(LOGS_FILE, 'w', encoding='utf-8') as f:
372
+ f.write('
373
+ '.join(logs_history[-1000:]))
374
  except Exception as e:
375
  print(f"[LOGS] Error saving logs: {e}", flush=True)
376
 
 
381
  return
382
  if not os.path.exists(LOGS_FILE):
383
  return
384
+
385
  with open(LOGS_FILE, 'r', encoding='utf-8') as f:
386
  content = f.read()
387
+
388
  api_url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/logs/console_logs.txt"
389
  headers = {
390
  "Authorization": f"token {GITHUB_TOKEN}",
391
  "Accept": "application/vnd.github.v3+json"
392
  }
393
+
394
  sha = None
395
  try:
396
  resp = requests.get(api_url, headers=headers, timeout=10)
 
398
  sha = resp.json().get('sha')
399
  except:
400
  pass
401
+
402
  import base64
403
  encoded_content = base64.b64encode(content.encode()).decode()
404
+
405
  data = {
406
  "message": f"Update logs - {format_sl_time()}",
407
  "content": encoded_content,
 
409
  }
410
  if sha:
411
  data["sha"] = sha
412
+
413
  resp = requests.put(api_url, headers=headers, json=data, timeout=10)
414
  if resp.status_code in [200, 201]:
415
  print("[LOGS] Pushed to GitHub successfully", flush=True)
 
454
  def start_minecraft():
455
  global mc_process, server_start_time
456
  mc_dir = "/app/mcserver"
457
+
458
  print("[MC] Starting Minecraft server...", flush=True)
459
  add_log_line("[SYSTEM] Starting Minecraft server...")
460
+
461
  server_start_time = time.time()
462
  save_uptime_to_file()
463
+
464
  mc_process = subprocess.Popen(
465
  ["java", "-Xms1G", "-Xmx14G", "-XX:+UseG1GC",
466
  "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200",
 
473
  bufsize=1,
474
  preexec_fn=os.setsid
475
  )
476
+
477
  print(f"[MC] Minecraft PID: {mc_process.pid}", flush=True)
478
  add_log_line(f"[SYSTEM] Minecraft server started with PID: {mc_process.pid}")
479
+
480
  def read_output():
481
  try:
482
  for line in mc_process.stdout:
 
489
  except Exception as e:
490
  print(f"[MC] Output error: {e}", flush=True)
491
  add_log_line(f"[ERROR] Output reader error: {e}")
492
+
493
  threading.Thread(target=read_output, daemon=True).start()
494
+
495
  def process_commands():
496
  while True:
497
  try:
 
509
  except Exception as e:
510
  print(f"[MC] Processor error: {e}", flush=True)
511
  break
512
+
513
  threading.Thread(target=process_commands, daemon=True).start()
514
 
515
  # ==========================================
516
+ # AUTHENTICATION ROUTES
517
+ # ==========================================
518
+ LOGIN_TEMPLATE = """
519
+ <!DOCTYPE html>
520
+ <html lang="en">
521
+ <head>
522
+ <meta charset="UTF-8">
523
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
524
+ <title>🔒 Minecraft Server - Secure Login</title>
525
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
526
+ <style>
527
+ * { margin: 0; padding: 0; box-sizing: border-box; }
528
+
529
+ body {
530
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
531
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
532
+ min-height: 100vh;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: center;
536
+ padding: 20px;
537
+ }
538
+
539
+ .login-container {
540
+ background: rgba(30, 41, 59, 0.95);
541
+ border: 1px solid rgba(148, 163, 184, 0.2);
542
+ border-radius: 16px;
543
+ padding: 40px;
544
+ width: 100%;
545
+ max-width: 420px;
546
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
547
+ backdrop-filter: blur(10px);
548
+ }
549
+
550
+ .login-header {
551
+ text-align: center;
552
+ margin-bottom: 30px;
553
+ }
554
+
555
+ .login-icon {
556
+ width: 80px;
557
+ height: 80px;
558
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
559
+ border-radius: 20px;
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: center;
563
+ margin: 0 auto 20px;
564
+ font-size: 36px;
565
+ color: white;
566
+ box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
567
+ }
568
+
569
+ .login-title {
570
+ font-size: 24px;
571
+ font-weight: 700;
572
+ color: #f8fafc;
573
+ margin-bottom: 8px;
574
+ }
575
+
576
+ .login-subtitle {
577
+ font-size: 14px;
578
+ color: #94a3b8;
579
+ }
580
+
581
+ .security-badge {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ gap: 6px;
585
+ background: rgba(34, 197, 94, 0.15);
586
+ color: #22c55e;
587
+ padding: 6px 12px;
588
+ border-radius: 20px;
589
+ font-size: 11px;
590
+ font-weight: 600;
591
+ margin-top: 12px;
592
+ border: 1px solid rgba(34, 197, 94, 0.3);
593
+ }
594
+
595
+ .form-group {
596
+ margin-bottom: 20px;
597
+ }
598
+
599
+ .form-label {
600
+ display: block;
601
+ font-size: 13px;
602
+ font-weight: 600;
603
+ color: #cbd5e1;
604
+ margin-bottom: 8px;
605
+ text-transform: uppercase;
606
+ letter-spacing: 0.5px;
607
+ }
608
+
609
+ .input-wrapper {
610
+ position: relative;
611
+ }
612
+
613
+ .input-wrapper i {
614
+ position: absolute;
615
+ left: 14px;
616
+ top: 50%;
617
+ transform: translateY(-50%);
618
+ color: #64748b;
619
+ font-size: 16px;
620
+ }
621
+
622
+ .form-input {
623
+ width: 100%;
624
+ padding: 14px 14px 14px 44px;
625
+ background: rgba(15, 23, 42, 0.8);
626
+ border: 2px solid rgba(148, 163, 184, 0.2);
627
+ border-radius: 10px;
628
+ color: #f1f5f9;
629
+ font-size: 15px;
630
+ outline: none;
631
+ transition: all 0.3s;
632
+ }
633
+
634
+ .form-input:focus {
635
+ border-color: #3b82f6;
636
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
637
+ }
638
+
639
+ .form-input::placeholder {
640
+ color: #475569;
641
+ }
642
+
643
+ .toggle-password {
644
+ position: absolute;
645
+ right: 14px;
646
+ top: 50%;
647
+ transform: translateY(-50%);
648
+ background: none;
649
+ border: none;
650
+ color: #64748b;
651
+ cursor: pointer;
652
+ font-size: 16px;
653
+ padding: 4px;
654
+ }
655
+
656
+ .toggle-password:hover {
657
+ color: #94a3b8;
658
+ }
659
+
660
+ .login-btn {
661
+ width: 100%;
662
+ padding: 14px;
663
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
664
+ color: white;
665
+ border: none;
666
+ border-radius: 10px;
667
+ font-size: 15px;
668
+ font-weight: 600;
669
+ cursor: pointer;
670
+ transition: all 0.3s;
671
+ display: flex;
672
+ align-items: center;
673
+ justify-content: center;
674
+ gap: 8px;
675
+ }
676
+
677
+ .login-btn:hover {
678
+ transform: translateY(-2px);
679
+ box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
680
+ }
681
+
682
+ .login-btn:active {
683
+ transform: translateY(0);
684
+ }
685
+
686
+ .login-btn:disabled {
687
+ opacity: 0.6;
688
+ cursor: not-allowed;
689
+ transform: none;
690
+ }
691
+
692
+ .error-message {
693
+ background: rgba(239, 68, 68, 0.15);
694
+ border: 1px solid rgba(239, 68, 68, 0.3);
695
+ color: #f87171;
696
+ padding: 12px;
697
+ border-radius: 8px;
698
+ font-size: 13px;
699
+ margin-bottom: 20px;
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 8px;
703
+ }
704
+
705
+ .error-message i {
706
+ font-size: 16px;
707
+ }
708
+
709
+ .info-box {
710
+ background: rgba(59, 130, 246, 0.1);
711
+ border: 1px solid rgba(59, 130, 246, 0.2);
712
+ border-radius: 8px;
713
+ padding: 12px;
714
+ margin-top: 20px;
715
+ font-size: 12px;
716
+ color: #94a3b8;
717
+ text-align: center;
718
+ }
719
+
720
+ .info-box i {
721
+ color: #3b82f6;
722
+ margin-right: 6px;
723
+ }
724
+
725
+ .attempts-warning {
726
+ background: rgba(234, 179, 8, 0.15);
727
+ border: 1px solid rgba(234, 179, 8, 0.3);
728
+ color: #eab308;
729
+ padding: 10px;
730
+ border-radius: 8px;
731
+ font-size: 12px;
732
+ margin-bottom: 20px;
733
+ text-align: center;
734
+ }
735
+
736
+ .lockout-message {
737
+ background: rgba(239, 68, 68, 0.15);
738
+ border: 1px solid rgba(239, 68, 68, 0.3);
739
+ color: #ef4444;
740
+ padding: 15px;
741
+ border-radius: 8px;
742
+ font-size: 14px;
743
+ margin-bottom: 20px;
744
+ text-align: center;
745
+ font-weight: 600;
746
+ }
747
+
748
+ @media (max-width: 480px) {
749
+ .login-container {
750
+ padding: 30px 20px;
751
+ }
752
+
753
+ .login-title {
754
+ font-size: 20px;
755
+ }
756
+
757
+ .login-icon {
758
+ width: 60px;
759
+ height: 60px;
760
+ font-size: 28px;
761
+ }
762
+ }
763
+ </style>
764
+ </head>
765
+ <body>
766
+ <div class="login-container">
767
+ <div class="login-header">
768
+ <div class="login-icon">
769
+ <i class="fas fa-shield-alt"></i>
770
+ </div>
771
+ <h1 class="login-title">Secure Access</h1>
772
+ <p class="login-subtitle">Minecraft Server Management Console</p>
773
+ <div class="security-badge">
774
+ <i class="fas fa-lock"></i> 256-bit Encryption
775
+ </div>
776
+ </div>
777
+
778
+ {% if error %}
779
+ <div class="error-message">
780
+ <i class="fas fa-exclamation-circle"></i>
781
+ {{ error }}
782
+ </div>
783
+ {% endif %}
784
+
785
+ {% if lockout %}
786
+ <div class="lockout-message">
787
+ <i class="fas fa-ban"></i>
788
+ Too many failed attempts. Please wait {{ lockout_time }} minutes.
789
+ </div>
790
+ {% endif %}
791
+
792
+ {% if attempts_left and attempts_left < MAX_LOGIN_ATTEMPTS %}
793
+ <div class="attempts-warning">
794
+ <i class="fas fa-exclamation-triangle"></i>
795
+ {{ attempts_left }} attempts remaining before lockout
796
+ </div>
797
+ {% endif %}
798
+
799
+ {% if not lockout %}
800
+ <form method="POST" action="/login" id="loginForm">
801
+ <div class="form-group">
802
+ <label class="form-label">Password</label>
803
+ <div class="input-wrapper">
804
+ <i class="fas fa-key"></i>
805
+ <input type="password" name="password" class="form-input" placeholder="Enter your password" required autocomplete="off" id="passwordInput">
806
+ <button type="button" class="toggle-password" onclick="togglePassword()">
807
+ <i class="fas fa-eye" id="toggleIcon"></i>
808
+ </button>
809
+ </div>
810
+ </div>
811
+
812
+ <button type="submit" class="login-btn" id="loginBtn">
813
+ <i class="fas fa-sign-in-alt"></i>
814
+ Secure Login
815
+ </button>
816
+ </form>
817
+ {% endif %}
818
+
819
+ <div class="info-box">
820
+ <i class="fas fa-info-circle"></i>
821
+ Session expires after 1 hour of inactivity
822
+ </div>
823
+ </div>
824
+
825
+ <script>
826
+ function togglePassword() {
827
+ const input = document.getElementById('passwordInput');
828
+ const icon = document.getElementById('toggleIcon');
829
+ if (input.type === 'password') {
830
+ input.type = 'text';
831
+ icon.className = 'fas fa-eye-slash';
832
+ } else {
833
+ input.type = 'password';
834
+ icon.className = 'fas fa-eye';
835
+ }
836
+ }
837
+
838
+ document.getElementById('loginForm').addEventListener('submit', function() {
839
+ document.getElementById('loginBtn').disabled = true;
840
+ document.getElementById('loginBtn').innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Authenticating...';
841
+ });
842
+ </script>
843
+ </body>
844
+ </html>
845
+ """
846
+
847
+ @app.route('/login', methods=['GET', 'POST'])
848
+ def login_page():
849
+ """Login page for password authentication"""
850
+ # If no password is configured, redirect to main app
851
+ if not WEB_PASSWORD:
852
+ return redirect(url_for('index'))
853
+
854
+ client_ip = get_client_ip()
855
+
856
+ # Check if IP is locked out
857
+ if is_locked_out(client_ip):
858
+ remaining = int((LOCKOUT_DURATION - (time.time() - login_attempts[client_ip]['last_attempt'])) / 60) + 1
859
+ return render_template_string(LOGIN_TEMPLATE,
860
+ error=None,
861
+ lockout=True,
862
+ lockout_time=remaining,
863
+ attempts_left=None,
864
+ MAX_LOGIN_ATTEMPTS=MAX_LOGIN_ATTEMPTS)
865
+
866
+ if request.method == 'POST':
867
+ password = request.form.get('password', '').strip()
868
+
869
+ if not password:
870
+ return render_template_string(LOGIN_TEMPLATE,
871
+ error="Password is required",
872
+ lockout=False,
873
+ attempts_left=MAX_LOGIN_ATTEMPTS - login_attempts.get(client_ip, {}).get('count', 0),
874
+ MAX_LOGIN_ATTEMPTS=MAX_LOGIN_ATTEMPTS)
875
+
876
+ if verify_password(password):
877
+ # Successful login
878
+ record_successful_attempt(client_ip)
879
+ session['authenticated'] = True
880
+ session['last_activity'] = time.time()
881
+ session.permanent = False
882
+ return redirect(url_for('index'))
883
+ else:
884
+ # Failed login
885
+ record_failed_attempt(client_ip)
886
+ remaining = MAX_LOGIN_ATTEMPTS - login_attempts[client_ip]['count']
887
+
888
+ if remaining <= 0:
889
+ return render_template_string(LOGIN_TEMPLATE,
890
+ error=None,
891
+ lockout=True,
892
+ lockout_time=int(LOCKOUT_DURATION/60),
893
+ attempts_left=None,
894
+ MAX_LOGIN_ATTEMPTS=MAX_LOGIN_ATTEMPTS)
895
+
896
+ return render_template_string(LOGIN_TEMPLATE,
897
+ error="Invalid password. Please try again.",
898
+ lockout=False,
899
+ attempts_left=remaining,
900
+ MAX_LOGIN_ATTEMPTS=MAX_LOGIN_ATTEMPTS)
901
+
902
+ return render_template_string(LOGIN_TEMPLATE,
903
+ error=None,
904
+ lockout=False,
905
+ attempts_left=None,
906
+ MAX_LOGIN_ATTEMPTS=MAX_LOGIN_ATTEMPTS)
907
+
908
+ @app.route('/logout')
909
+ def logout():
910
+ """Logout and clear session"""
911
+ session.clear()
912
+ return redirect(url_for('login_page'))
913
+
914
+ # ==========================================
915
+ # BACKUP API ENDPOINTS (PROTECTED)
916
  # ==========================================
917
  @app.route('/backup', methods=['POST'])
918
+ @login_required
919
  def manual_backup():
920
  """Trigger manual backup"""
921
  try:
 
935
  return {'success': False, 'error': str(e)}
936
 
937
  @app.route('/backup/status')
938
+ @login_required
939
  def backup_status():
940
  """Check backup/git status"""
941
  try:
 
977
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
978
  <style>
979
  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
980
+
981
  body {
982
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
983
  background: #1a1d21;
 
986
  overflow: hidden;
987
  touch-action: manipulation;
988
  }
989
+
 
990
  .top-nav {
991
  background: #252a30;
992
  border-bottom: 1px solid #363b42;
 
997
  position: relative;
998
  z-index: 100;
999
  }
1000
+
1001
  .nav-tabs { display: flex; gap: 0; height: 100%; overflow-x: auto; scrollbar-width: none; }
1002
  .nav-tabs::-webkit-scrollbar { display: none; }
1003
+
1004
  .nav-tab {
1005
  padding: 0 15px;
1006
  display: flex;
 
1015
  white-space: nowrap;
1016
  flex-shrink: 0;
1017
  }
1018
+
1019
  .nav-tab:hover { color: #e0e0e0; background: rgba(255,255,255,0.05); }
1020
  .nav-tab.active { color: #fff; border-bottom-color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
1021
  .nav-tab i { font-size: 14px; }
1022
+
1023
  .nav-right {
1024
  margin-left: auto;
1025
  display: flex;
1026
  align-items: center;
1027
  gap: 8px;
1028
  }
1029
+
1030
  .nav-btn {
1031
  background: transparent;
1032
  border: none;
 
1042
  justify-content: center;
1043
  position: relative;
1044
  }
1045
+
1046
  .nav-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
1047
+
1048
  .nav-btn .status-dot {
1049
  position: absolute;
1050
  bottom: 6px;
 
1055
  background: #22c55e;
1056
  border: 2px solid #252a30;
1057
  }
1058
+
1059
  .nav-btn .status-dot.offline { background: #ef4444; }
1060
+
1061
+ .logout-btn {
1062
+ background: transparent;
1063
+ border: 1px solid rgba(239, 68, 68, 0.4);
1064
+ color: #ef4444;
1065
+ padding: 6px 12px;
1066
+ border-radius: 6px;
1067
+ font-size: 12px;
1068
+ cursor: pointer;
1069
+ transition: all 0.2s;
1070
+ display: flex;
1071
+ align-items: center;
1072
+ gap: 5px;
1073
+ text-decoration: none;
1074
+ }
1075
+
1076
+ .logout-btn:hover {
1077
+ background: rgba(239, 68, 68, 0.15);
1078
+ border-color: rgba(239, 68, 68, 0.6);
1079
+ }
1080
+
1081
  .main-container {
1082
  display: flex;
1083
  height: calc(100vh - 50px);
1084
  flex-direction: row;
1085
  }
1086
+
 
1087
  .console-section {
1088
  flex: 1;
1089
  display: flex;
 
1091
  background: #0d1117;
1092
  min-width: 0;
1093
  }
1094
+
1095
  .console-header {
1096
  background: #161b22;
1097
  padding: 12px 15px;
 
1102
  flex-wrap: wrap;
1103
  gap: 10px;
1104
  }
1105
+
1106
  .server-title {
1107
  font-size: 16px;
1108
  font-weight: 600;
 
1111
  align-items: center;
1112
  gap: 8px;
1113
  }
1114
+
1115
  .server-title i { color: #3b82f6; font-size: 18px; }
1116
+
1117
  .control-buttons {
1118
  display: flex;
1119
  gap: 6px;
1120
  flex-wrap: wrap;
1121
  }
1122
+
1123
  .btn {
1124
  padding: 8px 14px;
1125
  border: none;
 
1134
  min-height: 36px;
1135
  touch-action: manipulation;
1136
  }
1137
+
1138
  .btn-primary { background: #2563eb; color: white; }
1139
  .btn-primary:hover { background: #1d4ed8; }
1140
  .btn-secondary { background: #374151; color: #e0e0e0; }
 
1144
  .btn-success { background: #16a34a; color: white; }
1145
  .btn-success:hover { background: #15803d; }
1146
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
1147
+
 
1148
  .terminal-container {
1149
  flex: 1;
1150
  background: #0d1117;
 
1152
  overflow: hidden;
1153
  position: relative;
1154
  }
1155
+
1156
  .terminal {
1157
  width: 100%;
1158
  height: 100%;
 
1166
  word-wrap: break-word;
1167
  -webkit-overflow-scrolling: touch;
1168
  }
1169
+
1170
  .terminal-line { margin: 2px 0; padding: 1px 0; }
1171
  .terminal-line.info { color: #58a6ff; }
1172
  .terminal-line.warn { color: #d29922; }
1173
  .terminal-line.error { color: #f85149; }
1174
  .terminal-line.success { color: #3fb950; }
1175
  .terminal-line.system { color: #a855f7; font-weight: 500; }
1176
+
1177
  .typing-preview {
1178
  color: #6e7681;
1179
  font-style: italic;
1180
  opacity: 0.8;
1181
  }
1182
+
1183
  .typing-preview .cursor {
1184
  animation: blink 1s infinite;
1185
  color: #58a6ff;
1186
  }
1187
+
1188
  @keyframes blink {
1189
  0%, 50% { opacity: 1; }
1190
  51%, 100% { opacity: 0; }
1191
  }
1192
+
 
1193
  .command-area {
1194
  background: #161b22;
1195
  border-top: 1px solid #30363d;
 
1198
  gap: 8px;
1199
  align-items: center;
1200
  }
1201
+
1202
  .command-input-wrapper {
1203
  flex: 1;
1204
  display: flex;
 
1210
  transition: border-color 0.2s;
1211
  min-height: 44px;
1212
  }
1213
+
1214
  .command-input-wrapper:focus-within { border-color: #58a6ff; }
1215
  .command-input-wrapper i { color: #6e7681; margin-right: 8px; font-size: 14px; }
1216
+
1217
  #commandInput {
1218
  flex: 1;
1219
  background: transparent;
 
1225
  outline: none;
1226
  min-width: 0;
1227
  }
1228
+
1229
  #commandInput::placeholder { color: #6e7681; }
1230
+
1231
  .send-btn {
1232
  background: #238636;
1233
  color: white;
 
1244
  font-size: 13px;
1245
  touch-action: manipulation;
1246
  }
1247
+
1248
  .send-btn:hover { background: #2ea043; }
1249
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1250
+
 
1251
  .sidebar {
1252
  width: 280px;
1253
  background: #1f2428;
 
1256
  overflow-y: auto;
1257
  -webkit-overflow-scrolling: touch;
1258
  }
1259
+
1260
  .stat-card {
1261
  background: #252a30;
1262
  border: 1px solid #363b42;
 
1268
  gap: 12px;
1269
  transition: transform 0.2s, box-shadow 0.2s;
1270
  }
1271
+
1272
  .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
1273
+
1274
  .stat-icon {
1275
  width: 40px;
1276
  height: 40px;
 
1281
  font-size: 18px;
1282
  flex-shrink: 0;
1283
  }
1284
+
1285
  .stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
1286
  .stat-icon.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
1287
  .stat-icon.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
1288
  .stat-icon.yellow { background: rgba(234, 179, 8, 0.2); color: #eab308; }
1289
  .stat-icon.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
1290
  .stat-icon.cyan { background: rgba(6, 182, 212, 0.2); color: #06b6d4; }
1291
+
1292
  .stat-info { flex: 1; min-width: 0; }
1293
  .stat-label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; }
1294
  .stat-value { font-size: 16px; font-weight: 600; color: #f0f6fc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1295
  .stat-sub { font-size: 10px; color: #6b7280; margin-top: 2px; }
1296
+
 
1297
  .status-indicator {
1298
  display: inline-flex;
1299
  align-items: center;
 
1304
  font-weight: 600;
1305
  text-transform: uppercase;
1306
  }
1307
+
1308
  .status-online { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
1309
  .status-offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
1310
  .status-starting { background: rgba(234, 179, 8, 0.2); color: #eab308; }
1311
+
1312
  .pulse { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
1313
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
1314
+
 
1315
  ::-webkit-scrollbar { width: 6px; height: 6px; }
1316
  ::-webkit-scrollbar-track { background: #1a1d21; }
1317
  ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; }
1318
  ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
1319
+
 
1320
  .connection-bar {
1321
  position: fixed;
1322
  top: 50px;
 
1331
  z-index: 99;
1332
  transition: all 0.3s;
1333
  }
1334
+
1335
  .connection-bar.connected { background: rgba(34, 197, 94, 0.15); color: #22c55e; border-bottom: 1px solid rgba(34, 197, 94, 0.3); }
1336
  .connection-bar.disconnected { background: rgba(239, 68, 68, 0.15); color: #ef4444; border-bottom: 1px solid rgba(239, 68, 68, 0.3); }
1337
  .connection-bar.connecting { background: rgba(234, 179, 8, 0.15); color: #eab308; border-bottom: 1px solid rgba(234, 179, 8, 0.3); }
1338
+
 
1339
  .sidebar-overlay {
1340
  display: none;
1341
  position: fixed;
 
1346
  background: rgba(0,0,0,0.5);
1347
  z-index: 150;
1348
  }
1349
+
1350
  .sidebar-overlay.active { display: block; }
1351
+
 
1352
  .status-modal {
1353
  display: none;
1354
  position: fixed;
 
1363
  min-width: 300px;
1364
  box-shadow: 0 10px 40px rgba(0,0,0,0.5);
1365
  }
1366
+
1367
  .status-modal.active { display: block; }
1368
+
1369
  .status-modal-header {
1370
  display: flex;
1371
  justify-content: space-between;
 
1374
  padding-bottom: 10px;
1375
  border-bottom: 1px solid #363b42;
1376
  }
1377
+
1378
  .status-modal-title {
1379
  font-size: 16px;
1380
  font-weight: 600;
1381
  color: #f0f6fc;
1382
  }
1383
+
1384
  .status-modal-close {
1385
  background: transparent;
1386
  border: none;
 
1388
  cursor: pointer;
1389
  font-size: 18px;
1390
  }
1391
+
1392
  .status-modal-close:hover { color: #fff; }
1393
+
1394
  .status-item {
1395
  display: flex;
1396
  justify-content: space-between;
1397
  padding: 8px 0;
1398
  border-bottom: 1px solid #2a2f35;
1399
  }
1400
+
1401
  .status-item:last-child { border-bottom: none; }
1402
+
1403
  .status-label-modal { color: #9ca3af; font-size: 13px; }
1404
  .status-value-modal { color: #f0f6fc; font-weight: 500; font-size: 13px; }
1405
+
1406
  .modal-overlay {
1407
  display: none;
1408
  position: fixed;
 
1413
  background: rgba(0,0,0,0.7);
1414
  z-index: 250;
1415
  }
1416
+
1417
  .modal-overlay.active { display: block; }
1418
+
 
1419
  .backup-section {
1420
  margin-top: 15px;
1421
  padding-top: 15px;
1422
  border-top: 1px solid #363b42;
1423
  }
1424
+
1425
  .backup-title {
1426
  font-size: 12px;
1427
  color: #9ca3af;
 
1432
  align-items: center;
1433
  gap: 6px;
1434
  }
1435
+
1436
  .backup-btn {
1437
  width: 100%;
1438
  padding: 10px;
 
1450
  transition: all 0.2s;
1451
  margin-bottom: 8px;
1452
  }
1453
+
1454
  .backup-btn:hover { background: #1e3a8a; }
1455
  .backup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1456
+
1457
  .backup-status {
1458
  font-size: 11px;
1459
  color: #6b7280;
1460
  text-align: center;
1461
  padding: 5px;
1462
  }
1463
+
1464
  .backup-status.success { color: #22c55e; }
1465
  .backup-status.error { color: #ef4444; }
1466
+
 
 
 
 
1467
  @media screen and (max-width: 768px) {
1468
  .sidebar {
1469
  position: fixed;
 
1475
  transition: right 0.3s ease;
1476
  box-shadow: -4px 0 20px rgba(0,0,0,0.5);
1477
  }
1478
+
1479
  .sidebar.active { right: 0; }
1480
  .connection-bar { right: 0; }
1481
+
1482
  .console-header { padding: 10px 12px; }
1483
  .server-title { font-size: 14px; }
1484
+
1485
  .btn {
1486
  padding: 6px 10px;
1487
  font-size: 11px;
1488
  min-height: 32px;
1489
  }
1490
+
1491
  .btn span { display: none; }
1492
+
1493
  .terminal {
1494
  font-size: 11px;
1495
  padding: 10px;
1496
  }
1497
+
1498
  .command-area { padding: 8px 10px; }
1499
+
1500
  .send-btn {
1501
  padding: 10px 14px;
1502
  font-size: 12px;
1503
  }
1504
+
1505
  .send-btn span { display: none; }
1506
  }
1507
+
1508
  @media screen and (max-width: 480px) {
1509
  .top-nav { padding: 0 10px; }
1510
+
1511
  .nav-tab {
1512
  padding: 0 10px;
1513
  font-size: 12px;
1514
  }
1515
+
1516
  .nav-tab i { font-size: 12px; }
1517
+
1518
  .console-header {
1519
  flex-direction: column;
1520
  align-items: flex-start;
1521
  gap: 8px;
1522
  }
1523
+
1524
  .control-buttons {
1525
  width: 100%;
1526
  justify-content: flex-start;
1527
  }
1528
+
1529
  .btn {
1530
  flex: 1;
1531
  justify-content: center;
1532
  padding: 8px;
1533
  }
1534
+
1535
  .terminal {
1536
  font-size: 10px;
1537
  line-height: 1.4;
1538
  }
1539
+
1540
  .command-input-wrapper { min-height: 40px; }
1541
  #commandInput { font-size: 16px; }
1542
+
1543
  .send-btn {
1544
  min-height: 40px;
1545
  padding: 8px 12px;
1546
  }
1547
+
1548
  .sidebar {
1549
  width: 100%;
1550
  right: -100%;
1551
  }
1552
+
1553
  .stat-card { padding: 10px; }
1554
+
1555
  .stat-icon {
1556
  width: 36px;
1557
  height: 36px;
1558
  font-size: 16px;
1559
  }
1560
+
1561
  .stat-value { font-size: 14px; }
1562
+
1563
  .status-modal {
1564
  min-width: 90vw;
1565
  max-width: 350px;
1566
  }
1567
  }
1568
+
1569
  @media screen and (max-height: 500px) and (orientation: landscape) {
1570
  .console-header { padding: 8px 12px; }
1571
  .command-area { padding: 6px 10px; }
1572
  .sidebar { padding: 10px; }
1573
  .stat-card { padding: 8px; margin-bottom: 6px; }
1574
  }
1575
+
1576
  @media screen and (min-width: 1400px) {
1577
  .sidebar { width: 320px; }
1578
  .terminal { font-size: 13px; }
 
1581
  </style>
1582
  </head>
1583
  <body>
 
1584
  <nav class="top-nav">
1585
  <div class="nav-tabs">
1586
  <a class="nav-tab active" onclick="switchTab('console', this)">
 
1598
  <button class="nav-btn" title="Full Screen" onclick="toggleFullscreen()">
1599
  <i class="fas fa-expand"></i>
1600
  </button>
1601
+ <a href="/logout" class="logout-btn" title="Logout">
1602
+ <i class="fas fa-sign-out-alt"></i> Logout
1603
+ </a>
1604
  </div>
1605
  </nav>
1606
+
 
1607
  <div class="modal-overlay" id="modalOverlay" onclick="toggleStatusModal()"></div>
1608
  <div class="status-modal" id="statusModal">
1609
  <div class="status-modal-header">
 
1649
  </div>
1650
  </div>
1651
  </div>
1652
+
 
1653
  <div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
1654
+
 
1655
  <div class="main-container">
 
1656
  <div class="console-section" id="consoleSection">
1657
  <div class="connection-bar connecting" id="connectionBar">
1658
  <i class="fas fa-circle-notch fa-spin"></i>
1659
  <span>Connecting to server...</span>
1660
  </div>
1661
+
1662
  <div class="console-header">
1663
  <div class="server-title">
1664
  <i class="fas fa-server"></i>
 
1679
  </button>
1680
  </div>
1681
  </div>
1682
+
1683
  <div class="terminal-container">
1684
  <div class="terminal" id="terminal"></div>
1685
  </div>
1686
+
1687
  <div class="command-area">
1688
  <div class="command-input-wrapper">
1689
  <i class="fas fa-angle-right"></i>
 
1694
  </button>
1695
  </div>
1696
  </div>
1697
+
 
1698
  <div class="sidebar" id="sidebar">
1699
  <div class="stat-card">
1700
  <div class="stat-icon blue"><i class="fas fa-network-wired"></i></div>
 
1704
  <div class="stat-sub">Port 25565</div>
1705
  </div>
1706
  </div>
1707
+
1708
  <div class="stat-card">
1709
  <div class="stat-icon red"><i class="fas fa-clock"></i></div>
1710
  <div class="stat-info">
 
1713
  <div class="stat-sub" id="uptimeSub">Not running</div>
1714
  </div>
1715
  </div>
1716
+
1717
  <div class="stat-card">
1718
  <div class="stat-icon yellow"><i class="fas fa-microchip"></i></div>
1719
  <div class="stat-info">
 
1722
  <div class="stat-sub" id="cpuSub">System average</div>
1723
  </div>
1724
  </div>
1725
+
1726
  <div class="stat-card">
1727
  <div class="stat-icon purple"><i class="fas fa-memory"></i></div>
1728
  <div class="stat-info">
 
1731
  <div class="stat-sub" id="memorySub">-- / -- GB</div>
1732
  </div>
1733
  </div>
1734
+
1735
  <div class="stat-card">
1736
  <div class="stat-icon cyan"><i class="fas fa-hdd"></i></div>
1737
  <div class="stat-info">
 
1740
  <div class="stat-sub">Server files</div>
1741
  </div>
1742
  </div>
1743
+
1744
  <div class="stat-card">
1745
  <div class="stat-icon green"><i class="fas fa-users"></i></div>
1746
  <div class="stat-info">
 
1749
  <div class="stat-sub">Online players</div>
1750
  </div>
1751
  </div>
1752
+
 
1753
  <div class="backup-section">
1754
  <div class="backup-title">
1755
  <i class="fas fa-cloud-upload-alt"></i> GitHub Backup
 
1780
  const navStatusDot = document.getElementById('navStatusDot');
1781
  const backupBtn = document.getElementById('backupBtn');
1782
  const backupStatus = document.getElementById('backupStatus');
1783
+
1784
  let ws = null;
1785
  let reconnectAttempts = 0;
1786
  let maxReconnectAttempts = 10;
 
1789
  let serverStartTime = null;
1790
  let typingPreviewLine = null;
1791
  let currentStats = {};
1792
+
1793
  function updateConnectionStatus(status, message) {
1794
  connectionBar.className = 'connection-bar ' + status;
1795
  const icons = {
 
1798
  'connecting': 'fa-circle-notch fa-spin'
1799
  };
1800
  connectionBar.innerHTML = '<i class="fas ' + icons[status] + '"></i><span>' + message + '</span>';
1801
+
1802
  const wsStatus = document.getElementById('modalWsStatus');
1803
  if (status === 'connected') {
1804
  wsStatus.innerHTML = '<span style="color: #22c55e;">● Connected</span>';
 
1808
  wsStatus.innerHTML = '<span style="color: #eab308;">● Connecting...</span>';
1809
  }
1810
  }
1811
+
1812
  function updateServerStatus(running, startTime = null) {
1813
  isServerRunning = running;
1814
  const statusEl = document.getElementById('serverStatus');
1815
+
1816
  if (running) {
1817
  statusEl.className = 'status-indicator status-online';
1818
  statusEl.innerHTML = '<span class="pulse"></span>Online';
 
1839
  document.getElementById('modalMcStatus').innerHTML = '<span style="color: #ef4444;">● Offline</span>';
1840
  }
1841
  }
1842
+
1843
  function addTerminalLine(text, type = '') {
1844
  if (typingPreviewLine && typingPreviewLine.parentNode) {
1845
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
1846
  typingPreviewLine = null;
1847
  }
1848
+
1849
  const line = document.createElement('div');
1850
  line.className = 'terminal-line ' + type;
1851
+
1852
  if (text.includes('[SYSTEM]')) {
1853
  line.className += ' system';
1854
  } else if (text.includes('> ')) {
 
1860
  } else if (text.toLowerCase().includes('done') || text.toLowerCase().includes('success')) {
1861
  line.className += ' success';
1862
  }
1863
+
1864
  line.textContent = text;
1865
  terminal.appendChild(line);
1866
  terminal.scrollTop = terminal.scrollHeight;
1867
+
1868
  while (terminal.children.length > 500) {
1869
  terminal.removeChild(terminal.firstChild);
1870
  }
1871
  }
1872
+
1873
  function updateTypingPreview() {
1874
  const text = commandInput.value.trim();
1875
+
1876
  if (typingPreviewLine && typingPreviewLine.parentNode) {
1877
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
1878
  typingPreviewLine = null;
1879
  }
1880
+
1881
  if (text && isServerRunning) {
1882
  typingPreviewLine = document.createElement('div');
1883
  typingPreviewLine.className = 'terminal-line typing-preview';
 
1886
  terminal.scrollTop = terminal.scrollHeight;
1887
  }
1888
  }
1889
+
1890
  function escapeHtml(text) {
1891
  const div = document.createElement('div');
1892
  div.textContent = text;
1893
  return div.innerHTML;
1894
  }
1895
+
1896
  function formatUptime(seconds) {
1897
  if (!seconds || seconds <= 0) return 'Offline';
1898
+
1899
  const days = Math.floor(seconds / 86400);
1900
  const hours = Math.floor((seconds % 86400) / 3600);
1901
  const minutes = Math.floor((seconds % 3600) / 60);
1902
  const secs = Math.floor(seconds % 60);
1903
+
1904
  if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm';
1905
  if (hours > 0) return hours + 'h ' + minutes + 'm ' + secs + 's';
1906
  if (minutes > 0) return minutes + 'm ' + secs + 's';
1907
  return secs + 's';
1908
  }
1909
+
1910
  function updateUptime() {
1911
  if (serverStartTime && isServerRunning) {
1912
  const now = Date.now();
 
1921
  document.getElementById('modalUptime').textContent = 'Offline';
1922
  }
1923
  }
1924
+
1925
  setInterval(updateUptime, 1000);
1926
+
 
 
 
1927
  async function triggerBackup() {
1928
  backupBtn.disabled = true;
1929
  backupStatus.textContent = 'Backing up...';
1930
  backupStatus.className = 'backup-status';
1931
+
1932
  try {
1933
  const response = await fetch('/backup', { method: 'POST' });
1934
  const result = await response.json();
1935
+
1936
  if (result.success) {
1937
  backupStatus.textContent = '✅ Backup successful!';
1938
  backupStatus.className = 'backup-status success';
 
1946
  backupStatus.textContent = '❌ Network error';
1947
  backupStatus.className = 'backup-status error';
1948
  }
1949
+
1950
  setTimeout(() => {
1951
  backupBtn.disabled = false;
1952
  }, 5000);
1953
  }
1954
+
1955
  async function checkBackupStatus() {
1956
  backupStatus.textContent = 'Checking...';
1957
+
1958
  try {
1959
  const response = await fetch('/backup/status');
1960
  const result = await response.json();
1961
+
1962
  if (result.error) {
1963
  backupStatus.textContent = '❌ Error: ' + result.error;
1964
  backupStatus.className = 'backup-status error';
1965
  } else if (result.has_changes) {
1966
+ backupStatus.textContent = '📝 Unsaved changes: ' + result.status.split('\n').length + ' files';
1967
  backupStatus.className = 'backup-status';
1968
  } else {
1969
  backupStatus.textContent = '✅ All changes synced';
 
1974
  backupStatus.className = 'backup-status error';
1975
  }
1976
  }
1977
+
1978
  function connect() {
1979
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1980
  ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
1981
+
1982
  ws.onopen = () => {
1983
  console.log('WebSocket connected');
1984
  reconnectAttempts = 0;
1985
  updateConnectionStatus('connected', 'Connected to server');
1986
  setTimeout(() => { connectionBar.style.opacity = '0'; }, 2000);
1987
  };
1988
+
1989
  ws.onmessage = (event) => {
1990
  const data = JSON.parse(event.data);
1991
+
1992
  if (data.type === 'output') {
1993
  addTerminalLine(data.data);
1994
  } else if (data.type === 'stats') {
 
1998
  const memTotal = (data.memoryTotal / 1024 / 1024 / 1024).toFixed(2);
1999
  const memUsed = ((data.memoryTotal * data.memory / 100) / 1024 / 1024 / 1024).toFixed(2);
2000
  document.getElementById('memorySub').textContent = memUsed + ' / ' + memTotal + ' GB';
2001
+
2002
  document.getElementById('modalCpu').textContent = data.cpu.toFixed(1) + '%';
2003
  document.getElementById('modalMemory').textContent = data.memory.toFixed(1) + '%';
2004
+
2005
  updateServerStatus(data.running, data.serverStartTime);
2006
+
2007
  if (data.players !== undefined) {
2008
  document.getElementById('playerCount').textContent = data.players;
2009
  document.getElementById('modalPlayers').textContent = data.players;
 
2014
  });
2015
  }
2016
  };
2017
+
2018
  ws.onclose = () => {
2019
  updateConnectionStatus('disconnected', 'Connection lost. Reconnecting...');
2020
  connectionBar.style.opacity = '1';
 
2024
  setTimeout(connect, 3000);
2025
  }
2026
  };
2027
+
2028
  ws.onerror = (error) => {
2029
  console.error('WebSocket error:', error);
2030
  };
2031
  }
2032
+
2033
  connect();
2034
+
2035
  function sendInput() {
2036
  const cmd = commandInput.value.trim();
2037
  if (cmd && ws && ws.readyState === WebSocket.OPEN) {
 
2043
  commandInput.value = '';
2044
  }
2045
  }
2046
+
2047
  function sendCmd(cmd) {
2048
  if (ws && ws.readyState === WebSocket.OPEN) {
2049
  ws.send(JSON.stringify({type: 'command', data: cmd}));
2050
  }
2051
  }
2052
+
2053
  commandInput.addEventListener('input', updateTypingPreview);
2054
  commandInput.addEventListener('keyup', updateTypingPreview);
2055
+
2056
  commandInput.addEventListener('keypress', (e) => {
2057
  if (e.key === 'Enter') {
2058
  if (typingPreviewLine && typingPreviewLine.parentNode) {
 
2062
  sendInput();
2063
  }
2064
  });
2065
+
2066
  commandInput.addEventListener('blur', () => {
2067
  if (typingPreviewLine && typingPreviewLine.parentNode) {
2068
  typingPreviewLine.parentNode.removeChild(typingPreviewLine);
2069
  typingPreviewLine = null;
2070
  }
2071
  });
2072
+
2073
  function switchTab(tab, element) {
2074
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
2075
  element.classList.add('active');
2076
  }
2077
+
2078
  function toggleFullscreen() {
2079
  if (!document.fullscreenElement) {
2080
  document.documentElement.requestFullscreen();
 
2082
  document.exitFullscreen();
2083
  }
2084
  }
2085
+
2086
  function toggleSidebar() {
2087
  sidebarOpen = !sidebarOpen;
2088
  if (sidebarOpen) {
 
2093
  sidebarOverlay.classList.remove('active');
2094
  }
2095
  }
2096
+
2097
  function toggleStatusModal() {
2098
  statusModal.classList.toggle('active');
2099
  modalOverlay.classList.toggle('active');
2100
  document.getElementById('modalAddress').textContent = window.location.hostname;
2101
  }
2102
+
2103
  document.addEventListener('click', (e) => {
2104
  if (window.innerWidth <= 768 && sidebarOpen) {
2105
  if (!sidebar.contains(e.target) && !e.target.closest('.sidebar-toggle')) {
 
2107
  }
2108
  }
2109
  });
2110
+
2111
  window.addEventListener('orientationchange', () => {
2112
  setTimeout(() => {
2113
  terminal.scrollTop = terminal.scrollHeight;
2114
  }, 300);
2115
  });
2116
+
2117
  let resizeTimer;
2118
  window.addEventListener('resize', () => {
2119
  clearTimeout(resizeTimer);
 
2125
  }
2126
  }, 250);
2127
  });
2128
+
2129
  let lastTouchEnd = 0;
2130
  document.addEventListener('touchend', (e) => {
2131
  const now = Date.now();
 
2134
  }
2135
  lastTouchEnd = now;
2136
  }, false);
2137
+
2138
  document.getElementById('serverAddress').textContent = window.location.hostname;
2139
  addTerminalLine('[SYSTEM] Connecting to Minecraft server...', 'system');
2140
  </script>
 
2143
  """
2144
 
2145
  @app.route('/')
2146
+ @login_required
2147
  def index():
2148
  return render_template_string(HTML_TEMPLATE)
2149
 
 
2157
  }
2158
 
2159
  @sock.route('/ws')
2160
+ @ws_login_required
2161
  def websocket(ws):
2162
  global server_start_time
2163
  print("[WS] Client connected", flush=True)
2164
+
2165
  try:
2166
  if logs_history:
2167
  ws.send(json.dumps({
 
2170
  }))
2171
  except Exception as e:
2172
  print(f"[WS] History send error: {e}", flush=True)
2173
+
2174
  def send_stats():
2175
  while True:
2176
  try:
2177
  cpu = psutil.cpu_percent(interval=0.5)
2178
  memory = psutil.virtual_memory()
2179
  running = mc_process is not None and mc_process.poll() is None
2180
+
2181
  uptime_to_send = None
2182
  if running and server_start_time:
2183
  uptime_to_send = server_start_time
2184
+
2185
  ws.send(json.dumps({
2186
  'type': 'stats',
2187
  'cpu': cpu,
 
2195
  print(f"[WS] Stats error: {e}", flush=True)
2196
  break
2197
  time.sleep(2)
2198
+
2199
  stats_thread = threading.Thread(target=send_stats, daemon=True)
2200
  stats_thread.start()
2201
+
2202
  def send_output():
2203
  while True:
2204
  try:
 
2211
  continue
2212
  except:
2213
  break
2214
+
2215
  output_thread = threading.Thread(target=send_output, daemon=True)
2216
  output_thread.start()
2217
+
2218
  while True:
2219
  try:
2220
  data = ws.receive(timeout=5)
 
2229
  if "Connection closed" in str(e):
2230
  break
2231
  continue
2232
+
2233
  print("[WS] Client disconnected", flush=True)
2234
 
2235
  def cleanup(signum=None, frame=None):
 
2253
  if __name__ == '__main__':
2254
  load_logs_from_file()
2255
  load_uptime_from_file()
2256
+
2257
  print(f"[MAIN] Starting web server... Timezone: {SL_TZ}", flush=True)
2258
+ print(f"[SECURITY] Password protection: {'ENABLED' if WEB_PASSWORD else 'DISABLED'}", flush=True)
2259
+
2260
  mc_thread = threading.Thread(target=start_minecraft, daemon=True)
2261
  mc_thread.start()
2262
+
2263
  def github_push_loop():
2264
  while True:
2265
  time.sleep(300)
2266
  push_logs_to_github()
2267
+
2268
  github_thread = threading.Thread(target=github_push_loop, daemon=True)
2269
  github_thread.start()
2270
+
2271
  from werkzeug.serving import run_simple
2272
  run_simple('0.0.0.0', 7860, app, threaded=True, use_reloader=False)
2273
  PYTHON_SCRIPT
2274
 
2275
+ echo "🌐 Starting Web Terminal Server with Password Protection..."
2276
  python3 /app/web_terminal.py 2>&1 &
2277
 
2278
  sleep 5
 
2287
  echo ""
2288
  echo "=========================================="
2289
  echo "✅ Minecraft Server with GitHub Backup is LIVE!"
2290
+ echo "🔒 Password Protection: ENABLED"
2291
  echo "🌐 Access at: https://your-space.hf.space"
2292
  echo "📱 Mobile optimized interface enabled"
2293
  echo "💾 Logs are now persistent across refreshes"
 
2303
  # Keep container alive
2304
  while true; do
2305
  sleep 30
2306
+
2307
  if ! pgrep -f "server.jar" > /dev/null; then
2308
  echo "⚠️ Minecraft server not running!"
2309
  fi
2310
+
2311
  if ! pgrep -f "web_terminal.py" > /dev/null; then
2312
  echo "❌ Web Terminal crashed! Restarting..."
2313
  python3 /app/web_terminal.py 2>&1 &