Update Dockerfile
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('\
|
| 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 &
|