Download Dockerfile from wpms/wpmss: direct link, hf CLI and curl.
- Browser
- Download file 69.2 kB
-
https://huggingface.co/spaces/wpms/wpmss/resolve/main/Dockerfile
- Command line
-
hf download hf://spaces/wpms/wpmss/Dockerfile
-
curl -L -o Dockerfile https://huggingface.co/spaces/wpms/wpmss/resolve/main/Dockerfile
69.2 kB
| FROM openjdk:21-jdk-slim | |
| # Install required packages | |
| RUN apt-get update && apt-get install -y \ | |
| python3 \ | |
| python3-pip \ | |
| curl \ | |
| git \ | |
| procps \ | |
| psmisc \ | |
| tzdata \ | |
| inotify-tools \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # Set Sri Lankan Timezone (Asia/Colombo, UTC+5:30) | |
| ENV TZ=Asia/Colombo | |
| RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone | |
| # Install Python dependencies | |
| RUN pip3 install flask pytz flask-sock psutil requests --break-system-packages | |
| WORKDIR /app | |
| # Create startup script | |
| RUN cat > start.sh << 'EOF' | |
| #!/bin/bash | |
| set -e | |
| cd /app | |
| echo "==========================================" | |
| echo "🚀 Starting Minecraft Server with Web Terminal" | |
| echo "⏰ $(date)" | |
| echo "🌍 Timezone: $(cat /etc/timezone) | $(date +%Z)" | |
| echo "==========================================" | |
| # Kill any existing processes on port 7860 | |
| fuser -k 7860/tcp 2>/dev/null || true | |
| sleep 2 | |
| # Clone private repo (requires GITHUB_TOKEN) | |
| if [ -n "$GITHUB_TOKEN" ]; then | |
| echo "📥 Cloning Minecraft server from GitHub..." | |
| rm -rf mcserver | |
| git clone https://$GITHUB_TOKEN@github.com/Walukapah/mcserver.git mcserver | |
| cd mcserver | |
| echo "✅ Repository cloned successfully" | |
| else | |
| echo "❌ ERROR: GITHUB_TOKEN not set!" | |
| exit 1 | |
| fi | |
| # Accept EULA if not exists | |
| if [ ! -f eula.txt ]; then | |
| echo "eula=true" > eula.txt | |
| echo "✅ EULA accepted" | |
| fi | |
| # ========================================== | |
| # CREATE RESTART SCRIPT (FIXED!) | |
| # ========================================== | |
| echo "🔄 Creating restart.sh script..." | |
| cat > restart.sh << 'RESTARTEOF' | |
| #!/bin/bash | |
| # Minecraft Server Restart Script | |
| # This script tells the Python web terminal to restart the server | |
| echo "[RESTART] Restart signal sent at $(date)" | |
| # Create a signal file that Python monitors | |
| touch /tmp/mc_restart_requested | |
| # Also kill the old java process after a short delay to ensure clean shutdown | |
| sleep 2 | |
| pkill -f "server.jar" 2>/dev/null || true | |
| RESTARTEOF | |
| chmod +x restart.sh | |
| echo "✅ restart.sh created and made executable" | |
| # ========================================== | |
| # GIT CONFIGURATION (VERY IMPORTANT!) | |
| # ========================================== | |
| echo "🔧 Setting up Git configuration..." | |
| git config --global user.email "minecraft@huggingface.co" | |
| git config --global user.name "Minecraft Bot" | |
| git config --global init.defaultBranch main | |
| # Ensure remote URL has token | |
| git remote set-url origin https://$GITHUB_TOKEN@github.com/Walukapah/mcserver.git | |
| # Create .gitignore to avoid unnecessary files | |
| cat > .gitignore << 'GITIGNORE' | |
| *.log | |
| logs/ | |
| cache/ | |
| tmp/ | |
| *.tmp | |
| restart.sh | |
| /tmp/ | |
| GITIGNORE | |
| echo "✅ Git configured successfully" | |
| # Create logs directory for persistent storage | |
| mkdir -p /app/logs | |
| # ========================================== | |
| # BACKUP SCRIPT (Fixed with Pull Before Push) | |
| # ========================================== | |
| cat > /app/mcserver/backup.sh << 'BACKUPSCRIPT' | |
| #!/bin/bash | |
| set -e | |
| echo "" | |
| echo "💾 [BACKUP] Starting backup at $(date '+%Y-%m-%d %H:%M:%S')" | |
| echo "📂 Current directory: $(pwd)" | |
| # Must be in mcserver directory | |
| cd /app/mcserver 2>/dev/null || { | |
| echo "❌ ERROR: Cannot find /app/mcserver directory!" | |
| exit 1 | |
| } | |
| # Check if git is initialized | |
| if [ ! -d .git ]; then | |
| echo "❌ ERROR: Not a git repository!" | |
| exit 1 | |
| fi | |
| # ========================================== | |
| # STEP 1: Fetch latest changes from GitHub | |
| # ========================================== | |
| echo "🔄 Fetching latest changes from GitHub..." | |
| git fetch origin main 2>&1 || git fetch origin master 2>&1 || { | |
| echo "⚠️ Warning: Could not fetch from remote" | |
| } | |
| # ========================================== | |
| # STEP 2: Check for remote changes and pull if needed | |
| # ========================================== | |
| LOCAL=$(git rev-parse HEAD 2>/dev/null || echo "unknown") | |
| REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown") | |
| if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then | |
| echo "📥 Remote has new changes, pulling first..." | |
| # Stash local changes, pull, then apply stash | |
| git stash push -m "auto-backup-stash" 2>/dev/null || true | |
| git pull origin main --rebase 2>&1 || git pull origin master --rebase 2>&1 || { | |
| echo "⚠️ Warning: Pull failed, trying merge..." | |
| git pull origin main 2>&1 || git pull origin master 2>&1 || true | |
| } | |
| # Try to pop stash | |
| git stash pop 2>/dev/null || true | |
| fi | |
| # ========================================== | |
| # STEP 3: Check for local changes | |
| # ========================================== | |
| if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then | |
| echo "ℹ️ No changes to backup" | |
| exit 0 | |
| fi | |
| echo "📤 Pushing changes to GitHub..." | |
| # Add all files including new ones | |
| git add -A | |
| # Commit with timestamp | |
| git commit -m "🎮 Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup" || { | |
| echo "ℹ️ Nothing new to commit" | |
| exit 0 | |
| } | |
| # ========================================== | |
| # STEP 4: Push with retry logic | |
| # ========================================== | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do | |
| # Fetch again to check if remote changed during commit | |
| git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true | |
| LOCAL=$(git rev-parse HEAD) | |
| REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown") | |
| if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then | |
| echo "🔄 Remote changed during commit, pulling again..." | |
| git stash push -m "auto-backup-stash-$RETRY_COUNT" 2>/dev/null || true | |
| git pull origin main --rebase 2>&1 || git pull origin master --rebase 2>&1 || { | |
| git pull origin main 2>&1 || git pull origin master 2>&1 || true | |
| } | |
| git stash pop 2>/dev/null || true | |
| # Re-commit if needed | |
| git add -A | |
| git commit -m "🎮 Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup (retry $RETRY_COUNT)" || true | |
| fi | |
| # Try to push | |
| if git push origin main 2>&1 || git push origin master 2>&1; then | |
| echo "✅ [BACKUP] Successfully pushed to GitHub!" | |
| echo "" | |
| exit 0 | |
| fi | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "⚠️ Push failed, retrying... ($RETRY_COUNT/$MAX_RETRIES)" | |
| sleep 2 | |
| done | |
| echo "❌ ERROR: Failed to push to GitHub after $MAX_RETRIES attempts!" | |
| echo "🔍 Local HEAD: $LOCAL" | |
| echo "🔍 Remote HEAD: $REMOTE" | |
| exit 1 | |
| BACKUPSCRIPT | |
| chmod +x /app/mcserver/backup.sh | |
| # ========================================== | |
| # BACKUP LOOP (Fixed - runs in correct directory) | |
| # ========================================== | |
| ( | |
| echo "⏰ Backup scheduler started (every 5 minutes)" | |
| while true; do | |
| sleep 5 | |
| /app/mcserver/backup.sh 2>&1 || true | |
| done | |
| ) & | |
| echo "✅ Auto-backup enabled (every 5 minutes)" | |
| echo "" | |
| # ========================================== | |
| # WEB TERMINAL SERVER (FIXED WITH AUTO-RESTART) | |
| # ========================================== | |
| cat > /app/web_terminal.py << 'PYTHON_SCRIPT' | |
| import os | |
| import sys | |
| import subprocess | |
| import json | |
| import threading | |
| import time | |
| import queue | |
| import signal | |
| import requests | |
| from flask import Flask, render_template_string | |
| from flask_sock import Sock | |
| import psutil | |
| from datetime import datetime, timezone, timedelta | |
| import pytz | |
| app = Flask(__name__) | |
| sock = Sock(app) | |
| mc_process = None | |
| output_queue = queue.Queue() | |
| command_queue = queue.Queue() | |
| server_ready = threading.Event() | |
| restart_lock = threading.Lock() | |
| # Persistent storage variables | |
| server_start_time = None | |
| logs_history = [] | |
| LOGS_FILE = "/app/logs/console_logs.txt" | |
| UPTIME_FILE = "/app/logs/server_start_time.txt" | |
| RESTART_SIGNAL_FILE = "/tmp/mc_restart_requested" | |
| GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN', '') | |
| GITHUB_REPO = "Walukapah/mcserver" | |
| # Sri Lankan Timezone | |
| SL_TZ = pytz.timezone('Asia/Colombo') | |
| def get_sl_time(): | |
| """Get current time in Sri Lankan timezone""" | |
| return datetime.now(SL_TZ) | |
| def format_sl_time(dt=None, fmt='%Y-%m-%d %H:%M:%S'): | |
| """Format datetime in Sri Lankan timezone""" | |
| if dt is None: | |
| dt = get_sl_time() | |
| return dt.strftime(fmt) | |
| def load_logs_from_file(): | |
| """Load previous logs from local file""" | |
| global logs_history | |
| try: | |
| if os.path.exists(LOGS_FILE): | |
| with open(LOGS_FILE, 'r', encoding='utf-8') as f: | |
| logs_history = f.read().splitlines() | |
| logs_history = logs_history[-1000:] | |
| print(f"[LOGS] Loaded {len(logs_history)} lines from file", flush=True) | |
| except Exception as e: | |
| print(f"[LOGS] Error loading logs: {e}", flush=True) | |
| logs_history = [] | |
| def save_logs_to_file(): | |
| """Save logs to local file""" | |
| try: | |
| os.makedirs(os.path.dirname(LOGS_FILE), exist_ok=True) | |
| with open(LOGS_FILE, 'w', encoding='utf-8') as f: | |
| f.write('\n'.join(logs_history[-1000:])) | |
| except Exception as e: | |
| print(f"[LOGS] Error saving logs: {e}", flush=True) | |
| def push_logs_to_github(): | |
| """Push logs to GitHub for persistent storage""" | |
| try: | |
| if not GITHUB_TOKEN: | |
| return | |
| if not os.path.exists(LOGS_FILE): | |
| return | |
| with open(LOGS_FILE, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| api_url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/logs/console_logs.txt" | |
| headers = { | |
| "Authorization": f"token {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| sha = None | |
| try: | |
| resp = requests.get(api_url, headers=headers, timeout=10) | |
| if resp.status_code == 200: | |
| sha = resp.json().get('sha') | |
| except: | |
| pass | |
| import base64 | |
| encoded_content = base64.b64encode(content.encode()).decode() | |
| data = { | |
| "message": f"Update logs - {format_sl_time()}", | |
| "content": encoded_content, | |
| "branch": "main" | |
| } | |
| if sha: | |
| data["sha"] = sha | |
| resp = requests.put(api_url, headers=headers, json=data, timeout=10) | |
| if resp.status_code in [200, 201]: | |
| print("[LOGS] Pushed to GitHub successfully", flush=True) | |
| except Exception as e: | |
| print(f"[LOGS] GitHub push error: {e}", flush=True) | |
| def load_uptime_from_file(): | |
| """Load server start time from file""" | |
| global server_start_time | |
| try: | |
| if os.path.exists(UPTIME_FILE): | |
| with open(UPTIME_FILE, 'r') as f: | |
| timestamp = f.read().strip() | |
| if timestamp: | |
| server_start_time = float(timestamp) | |
| print(f"[UPTIME] Loaded start time: {datetime.fromtimestamp(server_start_time, SL_TZ)}", flush=True) | |
| except Exception as e: | |
| print(f"[UPTIME] Error loading uptime: {e}", flush=True) | |
| server_start_time = None | |
| def save_uptime_to_file(): | |
| """Save server start time to file""" | |
| try: | |
| os.makedirs(os.path.dirname(UPTIME_FILE), exist_ok=True) | |
| with open(UPTIME_FILE, 'w') as f: | |
| f.write(str(server_start_time)) | |
| except Exception as e: | |
| print(f"[UPTIME] Error saving uptime: {e}", flush=True) | |
| def add_log_line(line): | |
| """Add a log line to history and save""" | |
| global logs_history | |
| timestamp = format_sl_time(fmt='%H:%M:%S') | |
| log_entry = f"[{timestamp}] {line}" | |
| logs_history.append(log_entry) | |
| if len(logs_history) > 1000: | |
| logs_history = logs_history[-1000:] | |
| output_queue.put(log_entry) | |
| if len(logs_history) % 10 == 0: | |
| save_logs_to_file() | |
| def create_restart_script(): | |
| """Create restart.sh if it doesn't exist""" | |
| mc_dir = "/app/mcserver" | |
| restart_script = os.path.join(mc_dir, "restart.sh") | |
| if not os.path.exists(restart_script): | |
| print("[MC] Creating restart.sh...", flush=True) | |
| with open(restart_script, 'w') as f: | |
| f.write('''#!/bin/bash | |
| echo "[RESTART] Restart signal sent at $(date)" | |
| touch /tmp/mc_restart_requested | |
| sleep 2 | |
| pkill -f "server.jar" 2>/dev/null || true | |
| ''') | |
| os.chmod(restart_script, 0o755) | |
| print("[MC] restart.sh created", flush=True) | |
| def start_minecraft(): | |
| global mc_process, server_start_time | |
| mc_dir = "/app/mcserver" | |
| with restart_lock: | |
| # Clear any old restart signal | |
| if os.path.exists(RESTART_SIGNAL_FILE): | |
| os.remove(RESTART_SIGNAL_FILE) | |
| print("[MC] Starting Minecraft server...", flush=True) | |
| add_log_line("[SYSTEM] Starting Minecraft server...") | |
| server_start_time = time.time() | |
| save_uptime_to_file() | |
| # Ensure restart.sh exists | |
| create_restart_script() | |
| mc_process = subprocess.Popen( | |
| ["java", "-Xms1G", "-Xmx14G", "-XX:+UseG1GC", | |
| "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200", | |
| "-jar", "server.jar", "nogui"], | |
| cwd=mc_dir, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| bufsize=1, | |
| preexec_fn=os.setsid | |
| ) | |
| print(f"[MC] Minecraft PID: {mc_process.pid}", flush=True) | |
| add_log_line(f"[SYSTEM] Minecraft server started with PID: {mc_process.pid}") | |
| def read_output(): | |
| try: | |
| for line in mc_process.stdout: | |
| line = line.rstrip() | |
| add_log_line(line) | |
| if "Done" in line and "For help, type" in line: | |
| server_ready.set() | |
| print("[MC] Server is ready!", flush=True) | |
| add_log_line("[SYSTEM] Server is ready!") | |
| except Exception as e: | |
| print(f"[MC] Output error: {e}", flush=True) | |
| add_log_line(f"[ERROR] Output reader error: {e}") | |
| threading.Thread(target=read_output, daemon=True).start() | |
| def process_commands(): | |
| while True: | |
| try: | |
| cmd = command_queue.get(timeout=1) | |
| if mc_process and mc_process.poll() is None: | |
| try: | |
| mc_process.stdin.write(cmd + "\n") | |
| mc_process.stdin.flush() | |
| print(f"[MC] Command: {cmd}", flush=True) | |
| add_log_line(f"> {cmd}") | |
| except Exception as e: | |
| print(f"[MC] Command error: {e}", flush=True) | |
| except queue.Empty: | |
| continue | |
| except Exception as e: | |
| print(f"[MC] Processor error: {e}", flush=True) | |
| break | |
| threading.Thread(target=process_commands, daemon=True).start() | |
| def stop_minecraft(): | |
| """Gracefully stop the Minecraft server""" | |
| global mc_process | |
| if mc_process and mc_process.poll() is None: | |
| print("[MC] Stopping Minecraft server...", flush=True) | |
| add_log_line("[SYSTEM] Stopping Minecraft server...") | |
| try: | |
| mc_process.stdin.write("stop\n") | |
| mc_process.stdin.flush() | |
| mc_process.wait(timeout=30) | |
| print("[MC] Server stopped gracefully", flush=True) | |
| add_log_line("[SYSTEM] Server stopped gracefully") | |
| except: | |
| try: | |
| os.killpg(os.getpgid(mc_process.pid), signal.SIGTERM) | |
| mc_process.wait(timeout=10) | |
| except: | |
| try: | |
| os.killpg(os.getpgid(mc_process.pid), signal.SIGKILL) | |
| except: | |
| pass | |
| mc_process = None | |
| def restart_minecraft(): | |
| """Restart the Minecraft server""" | |
| print("[RESTART] Restarting Minecraft server...", flush=True) | |
| add_log_line("[SYSTEM] Restarting Minecraft server...") | |
| stop_minecraft() | |
| time.sleep(3) | |
| start_minecraft() | |
| print("[RESTART] Server restart complete", flush=True) | |
| add_log_line("[SYSTEM] Server restart complete") | |
| def monitor_restart_signal(): | |
| """Monitor for restart signals from restart.sh or web UI""" | |
| while True: | |
| try: | |
| if os.path.exists(RESTART_SIGNAL_FILE): | |
| print("[MONITOR] Restart signal detected!", flush=True) | |
| os.remove(RESTART_SIGNAL_FILE) | |
| restart_minecraft() | |
| time.sleep(2) | |
| except Exception as e: | |
| print(f"[MONITOR] Error: {e}", flush=True) | |
| time.sleep(5) | |
| # ========================================== | |
| # BACKUP API ENDPOINTS | |
| # ========================================== | |
| @app.route('/backup', methods=['POST']) | |
| def manual_backup(): | |
| """Trigger manual backup""" | |
| try: | |
| result = subprocess.run( | |
| ['/app/mcserver/backup.sh'], | |
| capture_output=True, | |
| text=True, | |
| timeout=120, | |
| cwd='/app/mcserver' | |
| ) | |
| return { | |
| 'success': result.returncode == 0, | |
| 'output': result.stdout, | |
| 'error': result.stderr if result.stderr else None | |
| } | |
| except Exception as e: | |
| return {'success': False, 'error': str(e)} | |
| @app.route('/backup/status') | |
| def backup_status(): | |
| """Check backup/git status""" | |
| try: | |
| result = subprocess.run( | |
| ['git', 'status', '--short'], | |
| capture_output=True, | |
| text=True, | |
| timeout=10, | |
| cwd='/app/mcserver' | |
| ) | |
| branch_result = subprocess.run( | |
| ['git', 'branch', '--show-current'], | |
| capture_output=True, | |
| text=True, | |
| cwd='/app/mcserver' | |
| ) | |
| log_result = subprocess.run( | |
| ['git', 'log', '--oneline', '-5'], | |
| capture_output=True, | |
| text=True, | |
| cwd='/app/mcserver' | |
| ) | |
| return { | |
| 'has_changes': len(result.stdout.strip()) > 0, | |
| 'status': result.stdout, | |
| 'branch': branch_result.stdout.strip(), | |
| 'recent_commits': log_result.stdout.strip().split('\n') if log_result.stdout else [] | |
| } | |
| except Exception as e: | |
| return {'error': str(e)} | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Minecraft Server - Web Console</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: #1a1d21; | |
| color: #e0e0e0; | |
| height: 100vh; | |
| overflow: hidden; | |
| touch-action: manipulation; | |
| } | |
| /* Top Navigation */ | |
| .top-nav { | |
| background: #252a30; | |
| border-bottom: 1px solid #363b42; | |
| padding: 0 15px; | |
| display: flex; | |
| align-items: center; | |
| height: 50px; | |
| position: relative; | |
| z-index: 100; | |
| } | |
| .nav-tabs { display: flex; gap: 0; height: 100%; overflow-x: auto; scrollbar-width: none; } | |
| .nav-tabs::-webkit-scrollbar { display: none; } | |
| .nav-tab { | |
| padding: 0 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| color: #9ca3af; | |
| text-decoration: none; | |
| font-size: 13px; | |
| border-bottom: 2px solid transparent; | |
| transition: all 0.2s; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| flex-shrink: 0; | |
| } | |
| .nav-tab:hover { color: #e0e0e0; background: rgba(255,255,255,0.05); } | |
| .nav-tab.active { color: #fff; border-bottom-color: #3b82f6; background: rgba(59, 130, 246, 0.1); } | |
| .nav-tab i { font-size: 14px; } | |
| .nav-right { | |
| margin-left: auto; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .nav-btn { | |
| background: transparent; | |
| border: none; | |
| color: #9ca3af; | |
| padding: 8px; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| min-width: 36px; | |
| min-height: 36px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .nav-btn:hover { color: #fff; background: rgba(255,255,255,0.1); } | |
| .nav-btn .status-dot { | |
| position: absolute; | |
| bottom: 6px; | |
| right: 6px; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #22c55e; | |
| border: 2px solid #252a30; | |
| } | |
| .nav-btn .status-dot.offline { background: #ef4444; } | |
| /* Main Layout */ | |
| .main-container { | |
| display: flex; | |
| height: calc(100vh - 50px); | |
| flex-direction: row; | |
| } | |
| /* Console Area */ | |
| .console-section { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: #0d1117; | |
| min-width: 0; | |
| } | |
| .console-header { | |
| background: #161b22; | |
| padding: 12px 15px; | |
| border-bottom: 1px solid #30363d; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .server-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #f0f6fc; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .server-title i { color: #3b82f6; font-size: 18px; } | |
| .control-buttons { | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| padding: 8px 14px; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| transition: all 0.2s; | |
| min-height: 36px; | |
| touch-action: manipulation; | |
| } | |
| .btn-primary { background: #2563eb; color: white; } | |
| .btn-primary:hover { background: #1d4ed8; } | |
| .btn-secondary { background: #374151; color: #e0e0e0; } | |
| .btn-secondary:hover { background: #4b5563; } | |
| .btn-danger { background: #dc2626; color: white; } | |
| .btn-danger:hover { background: #b91c1c; } | |
| .btn-success { background: #16a34a; color: white; } | |
| .btn-success:hover { background: #15803d; } | |
| .btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| /* Terminal */ | |
| .terminal-container { | |
| flex: 1; | |
| background: #0d1117; | |
| padding: 0; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .terminal { | |
| width: 100%; | |
| height: 100%; | |
| padding: 12px; | |
| font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: #e6edf3; | |
| overflow-y: auto; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .terminal-line { margin: 2px 0; padding: 1px 0; } | |
| .terminal-line.info { color: #58a6ff; } | |
| .terminal-line.warn { color: #d29922; } | |
| .terminal-line.error { color: #f85149; } | |
| .terminal-line.success { color: #3fb950; } | |
| .terminal-line.system { color: #a855f7; font-weight: 500; } | |
| .typing-preview { | |
| color: #6e7681; | |
| font-style: italic; | |
| opacity: 0.8; | |
| } | |
| .typing-preview .cursor { | |
| animation: blink 1s infinite; | |
| color: #58a6ff; | |
| } | |
| @keyframes blink { | |
| 0%, 50% { opacity: 1; } | |
| 51%, 100% { opacity: 0; } | |
| } | |
| /* Command Input Area */ | |
| .command-area { | |
| background: #161b22; | |
| border-top: 1px solid #30363d; | |
| padding: 10px 12px; | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .command-input-wrapper { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| background: #0d1117; | |
| border: 1px solid #30363d; | |
| border-radius: 6px; | |
| padding: 0 12px; | |
| transition: border-color 0.2s; | |
| min-height: 44px; | |
| } | |
| .command-input-wrapper:focus-within { border-color: #58a6ff; } | |
| .command-input-wrapper i { color: #6e7681; margin-right: 8px; font-size: 14px; } | |
| #commandInput { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: #e6edf3; | |
| padding: 10px 0; | |
| font-size: 14px; | |
| font-family: 'SFMono-Regular', Consolas, monospace; | |
| outline: none; | |
| min-width: 0; | |
| } | |
| #commandInput::placeholder { color: #6e7681; } | |
| .send-btn { | |
| background: #238636; | |
| color: white; | |
| border: none; | |
| padding: 12px 18px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| transition: background 0.2s; | |
| min-height: 44px; | |
| font-size: 13px; | |
| touch-action: manipulation; | |
| } | |
| .send-btn:hover { background: #2ea043; } | |
| .send-btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| /* Sidebar */ | |
| .sidebar { | |
| width: 280px; | |
| background: #1f2428; | |
| border-left: 1px solid #363b42; | |
| padding: 15px; | |
| overflow-y: auto; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .stat-card { | |
| background: #252a30; | |
| border: 1px solid #363b42; | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); } | |
| .stat-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| flex-shrink: 0; | |
| } | |
| .stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; } | |
| .stat-icon.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; } | |
| .stat-icon.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; } | |
| .stat-icon.yellow { background: rgba(234, 179, 8, 0.2); color: #eab308; } | |
| .stat-icon.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; } | |
| .stat-icon.cyan { background: rgba(6, 182, 212, 0.2); color: #06b6d4; } | |
| .stat-info { flex: 1; min-width: 0; } | |
| .stat-label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; } | |
| .stat-value { font-size: 16px; font-weight: 600; color: #f0f6fc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .stat-sub { font-size: 10px; color: #6b7280; margin-top: 2px; } | |
| /* Status Badge */ | |
| .status-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 8px; | |
| border-radius: 20px; | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .status-online { background: rgba(34, 197, 94, 0.2); color: #22c55e; } | |
| .status-offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; } | |
| .status-starting { background: rgba(234, 179, 8, 0.2); color: #eab308; } | |
| .pulse { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: #1a1d21; } | |
| ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #6b7280; } | |
| /* Connection Status */ | |
| .connection-bar { | |
| position: fixed; | |
| top: 50px; | |
| left: 0; | |
| right: 280px; | |
| padding: 6px 15px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| z-index: 99; | |
| transition: all 0.3s; | |
| } | |
| .connection-bar.connected { background: rgba(34, 197, 94, 0.15); color: #22c55e; border-bottom: 1px solid rgba(34, 197, 94, 0.3); } | |
| .connection-bar.disconnected { background: rgba(239, 68, 68, 0.15); color: #ef4444; border-bottom: 1px solid rgba(239, 68, 68, 0.3); } | |
| .connection-bar.connecting { background: rgba(234, 179, 8, 0.15); color: #eab308; border-bottom: 1px solid rgba(234, 179, 8, 0.3); } | |
| /* Mobile Overlay */ | |
| .sidebar-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 150; | |
| } | |
| .sidebar-overlay.active { display: block; } | |
| /* Status Modal */ | |
| .status-modal { | |
| display: none; | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: #1f2428; | |
| border: 1px solid #363b42; | |
| border-radius: 12px; | |
| padding: 20px; | |
| z-index: 300; | |
| min-width: 300px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.5); | |
| } | |
| .status-modal.active { display: block; } | |
| .status-modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid #363b42; | |
| } | |
| .status-modal-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #f0f6fc; | |
| } | |
| .status-modal-close { | |
| background: transparent; | |
| border: none; | |
| color: #9ca3af; | |
| cursor: pointer; | |
| font-size: 18px; | |
| } | |
| .status-modal-close:hover { color: #fff; } | |
| .status-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| border-bottom: 1px solid #2a2f35; | |
| } | |
| .status-item:last-child { border-bottom: none; } | |
| .status-label-modal { color: #9ca3af; font-size: 13px; } | |
| .status-value-modal { color: #f0f6fc; font-weight: 500; font-size: 13px; } | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.7); | |
| z-index: 250; | |
| } | |
| .modal-overlay.active { display: block; } | |
| /* Backup Section */ | |
| .backup-section { | |
| margin-top: 15px; | |
| padding-top: 15px; | |
| border-top: 1px solid #363b42; | |
| } | |
| .backup-title { | |
| font-size: 12px; | |
| color: #9ca3af; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .backup-btn { | |
| width: 100%; | |
| padding: 10px; | |
| background: #1e40af; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| transition: all 0.2s; | |
| margin-bottom: 8px; | |
| } | |
| .backup-btn:hover { background: #1e3a8a; } | |
| .backup-btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .backup-status { | |
| font-size: 11px; | |
| color: #6b7280; | |
| text-align: center; | |
| padding: 5px; | |
| } | |
| .backup-status.success { color: #22c55e; } | |
| .backup-status.error { color: #ef4444; } | |
| /* ============================================ | |
| MOBILE RESPONSIVE BREAKPOINTS | |
| ============================================ */ | |
| @media screen and (max-width: 768px) { | |
| .sidebar { | |
| position: fixed; | |
| right: -100%; | |
| top: 50px; | |
| height: calc(100vh - 50px); | |
| width: 280px; | |
| z-index: 160; | |
| transition: right 0.3s ease; | |
| box-shadow: -4px 0 20px rgba(0,0,0,0.5); | |
| } | |
| .sidebar.active { right: 0; } | |
| .connection-bar { right: 0; } | |
| .console-header { padding: 10px 12px; } | |
| .server-title { font-size: 14px; } | |
| .btn { | |
| padding: 6px 10px; | |
| font-size: 11px; | |
| min-height: 32px; | |
| } | |
| .btn span { display: none; } | |
| .terminal { | |
| font-size: 11px; | |
| padding: 10px; | |
| } | |
| .command-area { padding: 8px 10px; } | |
| .send-btn { | |
| padding: 10px 14px; | |
| font-size: 12px; | |
| } | |
| .send-btn span { display: none; } | |
| } | |
| @media screen and (max-width: 480px) { | |
| .top-nav { padding: 0 10px; } | |
| .nav-tab { | |
| padding: 0 10px; | |
| font-size: 12px; | |
| } | |
| .nav-tab i { font-size: 12px; } | |
| .console-header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 8px; | |
| } | |
| .control-buttons { | |
| width: 100%; | |
| justify-content: flex-start; | |
| } | |
| .btn { | |
| flex: 1; | |
| justify-content: center; | |
| padding: 8px; | |
| } | |
| .terminal { | |
| font-size: 10px; | |
| line-height: 1.4; | |
| } | |
| .command-input-wrapper { min-height: 40px; } | |
| #commandInput { font-size: 16px; } | |
| .send-btn { | |
| min-height: 40px; | |
| padding: 8px 12px; | |
| } | |
| .sidebar { | |
| width: 100%; | |
| right: -100%; | |
| } | |
| .stat-card { padding: 10px; } | |
| .stat-icon { | |
| width: 36px; | |
| height: 36px; | |
| font-size: 16px; | |
| } | |
| .stat-value { font-size: 14px; } | |
| .status-modal { | |
| min-width: 90vw; | |
| max-width: 350px; | |
| } | |
| } | |
| @media screen and (max-height: 500px) and (orientation: landscape) { | |
| .console-header { padding: 8px 12px; } | |
| .command-area { padding: 6px 10px; } | |
| .sidebar { padding: 10px; } | |
| .stat-card { padding: 8px; margin-bottom: 6px; } | |
| } | |
| @media screen and (min-width: 1400px) { | |
| .sidebar { width: 320px; } | |
| .terminal { font-size: 13px; } | |
| .stat-value { font-size: 18px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Top Navigation --> | |
| <nav class="top-nav"> | |
| <div class="nav-tabs"> | |
| <a class="nav-tab active" onclick="switchTab('console', this)"> | |
| <i class="fas fa-terminal"></i> Console | |
| </a> | |
| <a class="nav-tab" onclick="switchTab('files', this)"> | |
| <i class="fas fa-folder"></i> Files | |
| </a> | |
| </div> | |
| <div class="nav-right"> | |
| <button class="nav-btn" id="statusBtn" title="Server Status" onclick="toggleStatusModal()"> | |
| <i class="fas fa-heartbeat"></i> | |
| <span class="status-dot offline" id="navStatusDot"></span> | |
| </button> | |
| <button class="nav-btn" title="Full Screen" onclick="toggleFullscreen()"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| </div> | |
| </nav> | |
| <!-- Status Modal --> | |
| <div class="modal-overlay" id="modalOverlay" onclick="toggleStatusModal()"></div> | |
| <div class="status-modal" id="statusModal"> | |
| <div class="status-modal-header"> | |
| <div class="status-modal-title"> | |
| <i class="fas fa-server"></i> Server Status | |
| </div> | |
| <button class="status-modal-close" onclick="toggleStatusModal()"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="status-content"> | |
| <div class="status-item"> | |
| <span class="status-label-modal">WebSocket</span> | |
| <span class="status-value-modal" id="modalWsStatus">Connecting...</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Minecraft Server</span> | |
| <span class="status-value-modal" id="modalMcStatus">Offline</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Server Uptime</span> | |
| <span class="status-value-modal" id="modalUptime">--</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">CPU Usage</span> | |
| <span class="status-value-modal" id="modalCpu">--%</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Memory Usage</span> | |
| <span class="status-value-modal" id="modalMemory">--%</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Active Players</span> | |
| <span class="status-value-modal" id="modalPlayers">--</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Server Address</span> | |
| <span class="status-value-modal" id="modalAddress">--</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label-modal">Timezone</span> | |
| <span class="status-value-modal" id="modalTimezone">Asia/Colombo (UTC+5:30)</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sidebar Overlay --> | |
| <div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div> | |
| <!-- Main Container --> | |
| <div class="main-container"> | |
| <!-- Console Section --> | |
| <div class="console-section" id="consoleSection"> | |
| <div class="connection-bar connecting" id="connectionBar"> | |
| <i class="fas fa-circle-notch fa-spin"></i> | |
| <span>Connecting to server...</span> | |
| </div> | |
| <div class="console-header"> | |
| <div class="server-title"> | |
| <i class="fas fa-server"></i> | |
| <span>Minecraft Server</span> | |
| <span class="status-indicator status-offline" id="serverStatus"> | |
| <span class="pulse"></span> Offline | |
| </span> | |
| </div> | |
| <div class="control-buttons"> | |
| <button class="btn btn-primary" id="startBtn" onclick="sendCmd('start')" disabled> | |
| <i class="fas fa-play"></i> <span>Start</span> | |
| </button> | |
| <button class="btn btn-secondary" id="restartBtn" onclick="sendCmd('restart')" disabled> | |
| <i class="fas fa-redo"></i> <span>Restart</span> | |
| </button> | |
| <button class="btn btn-danger" id="stopBtn" onclick="sendCmd('stop')" disabled> | |
| <i class="fas fa-stop"></i> <span>Stop</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="terminal-container"> | |
| <div class="terminal" id="terminal"></div> | |
| </div> | |
| <div class="command-area"> | |
| <div class="command-input-wrapper"> | |
| <i class="fas fa-angle-right"></i> | |
| <input type="text" id="commandInput" placeholder="Type a command..." autocomplete="off" disabled> | |
| </div> | |
| <button class="send-btn" id="sendBtn" onclick="sendInput()" disabled> | |
| <i class="fas fa-paper-plane"></i> <span>Send</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="stat-card"> | |
| <div class="stat-icon blue"><i class="fas fa-network-wired"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">Address</div> | |
| <div class="stat-value" id="serverAddress">--</div> | |
| <div class="stat-sub">Port 25565</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon red"><i class="fas fa-clock"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">Uptime</div> | |
| <div class="stat-value" id="uptime">Offline</div> | |
| <div class="stat-sub" id="uptimeSub">Not running</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon yellow"><i class="fas fa-microchip"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">CPU Load</div> | |
| <div class="stat-value" id="cpuLoad">--%</div> | |
| <div class="stat-sub" id="cpuSub">System average</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon purple"><i class="fas fa-memory"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">Memory</div> | |
| <div class="stat-value" id="memoryUsage">--%</div> | |
| <div class="stat-sub" id="memorySub">-- / -- GB</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon cyan"><i class="fas fa-hdd"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">Disk</div> | |
| <div class="stat-value" id="diskUsage">-- MB</div> | |
| <div class="stat-sub">Server files</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon green"><i class="fas fa-users"></i></div> | |
| <div class="stat-info"> | |
| <div class="stat-label">Players</div> | |
| <div class="stat-value" id="playerCount">--</div> | |
| <div class="stat-sub">Online players</div> | |
| </div> | |
| </div> | |
| <!-- Backup Section --> | |
| <div class="backup-section"> | |
| <div class="backup-title"> | |
| <i class="fas fa-cloud-upload-alt"></i> GitHub Backup | |
| </div> | |
| <button class="backup-btn" id="backupBtn" onclick="triggerBackup()"> | |
| <i class="fas fa-save"></i> Backup Now | |
| </button> | |
| <button class="backup-btn" style="background: #374151;" onclick="checkBackupStatus()"> | |
| <i class="fas fa-info-circle"></i> Check Status | |
| </button> | |
| <div class="backup-status" id="backupStatus">Auto-backup every 5 min</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const terminal = document.getElementById('terminal'); | |
| const commandInput = document.getElementById('commandInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const connectionBar = document.getElementById('connectionBar'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const sidebarOverlay = document.getElementById('sidebarOverlay'); | |
| const statusModal = document.getElementById('statusModal'); | |
| const modalOverlay = document.getElementById('modalOverlay'); | |
| const navStatusDot = document.getElementById('navStatusDot'); | |
| const backupBtn = document.getElementById('backupBtn'); | |
| const backupStatus = document.getElementById('backupStatus'); | |
| let ws = null; | |
| let reconnectAttempts = 0; | |
| let maxReconnectAttempts = 10; | |
| let isServerRunning = false; | |
| let sidebarOpen = false; | |
| let serverStartTime = null; | |
| let typingPreviewLine = null; | |
| let currentStats = {}; | |
| function updateConnectionStatus(status, message) { | |
| connectionBar.className = 'connection-bar ' + status; | |
| const icons = { | |
| 'connected': 'fa-check-circle', | |
| 'disconnected': 'fa-exclamation-triangle', | |
| 'connecting': 'fa-circle-notch fa-spin' | |
| }; | |
| connectionBar.innerHTML = '<i class="fas ' + icons[status] + '"></i><span>' + message + '</span>'; | |
| const wsStatus = document.getElementById('modalWsStatus'); | |
| if (status === 'connected') { | |
| wsStatus.innerHTML = '<span style="color: #22c55e;">● Connected</span>'; | |
| } else if (status === 'disconnected') { | |
| wsStatus.innerHTML = '<span style="color: #ef4444;">● Disconnected</span>'; | |
| } else { | |
| wsStatus.innerHTML = '<span style="color: #eab308;">● Connecting...</span>'; | |
| } | |
| } | |
| function updateServerStatus(running, startTime = null) { | |
| isServerRunning = running; | |
| const statusEl = document.getElementById('serverStatus'); | |
| if (running) { | |
| statusEl.className = 'status-indicator status-online'; | |
| statusEl.innerHTML = '<span class="pulse"></span>Online'; | |
| startBtn.disabled = true; | |
| restartBtn.disabled = false; | |
| stopBtn.disabled = false; | |
| commandInput.disabled = false; | |
| sendBtn.disabled = false; | |
| navStatusDot.className = 'status-dot'; | |
| if (startTime) { | |
| serverStartTime = startTime * 1000; | |
| } | |
| document.getElementById('modalMcStatus').innerHTML = '<span style="color: #22c55e;">● Online</span>'; | |
| } else { | |
| statusEl.className = 'status-indicator status-offline'; | |
| statusEl.innerHTML = '<span class="pulse"></span>Offline'; | |
| startBtn.disabled = false; | |
| restartBtn.disabled = true; | |
| stopBtn.disabled = true; | |
| commandInput.disabled = true; | |
| sendBtn.disabled = true; | |
| navStatusDot.className = 'status-dot offline'; | |
| serverStartTime = null; | |
| document.getElementById('modalMcStatus').innerHTML = '<span style="color: #ef4444;">● Offline</span>'; | |
| } | |
| } | |
| function addTerminalLine(text, type = '') { | |
| if (typingPreviewLine && typingPreviewLine.parentNode) { | |
| typingPreviewLine.parentNode.removeChild(typingPreviewLine); | |
| typingPreviewLine = null; | |
| } | |
| const line = document.createElement('div'); | |
| line.className = 'terminal-line ' + type; | |
| if (text.includes('[SYSTEM]')) { | |
| line.className += ' system'; | |
| } else if (text.includes('> ')) { | |
| line.className += ' info'; | |
| } else if (text.toLowerCase().includes('error') || text.toLowerCase().includes('exception')) { | |
| line.className += ' error'; | |
| } else if (text.toLowerCase().includes('warn')) { | |
| line.className += ' warn'; | |
| } else if (text.toLowerCase().includes('done') || text.toLowerCase().includes('success')) { | |
| line.className += ' success'; | |
| } | |
| line.textContent = text; | |
| terminal.appendChild(line); | |
| terminal.scrollTop = terminal.scrollHeight; | |
| while (terminal.children.length > 500) { | |
| terminal.removeChild(terminal.firstChild); | |
| } | |
| } | |
| function updateTypingPreview() { | |
| const text = commandInput.value.trim(); | |
| if (typingPreviewLine && typingPreviewLine.parentNode) { | |
| typingPreviewLine.parentNode.removeChild(typingPreviewLine); | |
| typingPreviewLine = null; | |
| } | |
| if (text && isServerRunning) { | |
| typingPreviewLine = document.createElement('div'); | |
| typingPreviewLine.className = 'terminal-line typing-preview'; | |
| typingPreviewLine.innerHTML = '> ' + escapeHtml(text) + '<span class="cursor">_</span>'; | |
| terminal.appendChild(typingPreviewLine); | |
| terminal.scrollTop = terminal.scrollHeight; | |
| } | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function formatUptime(seconds) { | |
| if (!seconds || seconds <= 0) return 'Offline'; | |
| const days = Math.floor(seconds / 86400); | |
| const hours = Math.floor((seconds % 86400) / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = Math.floor(seconds % 60); | |
| if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm'; | |
| if (hours > 0) return hours + 'h ' + minutes + 'm ' + secs + 's'; | |
| if (minutes > 0) return minutes + 'm ' + secs + 's'; | |
| return secs + 's'; | |
| } | |
| function updateUptime() { | |
| if (serverStartTime && isServerRunning) { | |
| const now = Date.now(); | |
| const uptime = Math.floor((now - serverStartTime) / 1000); | |
| const formatted = formatUptime(uptime); | |
| document.getElementById('uptime').textContent = formatted; | |
| document.getElementById('uptimeSub').textContent = 'Running'; | |
| document.getElementById('modalUptime').textContent = formatted; | |
| } else { | |
| document.getElementById('uptime').textContent = 'Offline'; | |
| document.getElementById('uptimeSub').textContent = 'Not running'; | |
| document.getElementById('modalUptime').textContent = 'Offline'; | |
| } | |
| } | |
| setInterval(updateUptime, 1000); | |
| // ========================================== | |
| // BACKUP FUNCTIONS | |
| // ========================================== | |
| async function triggerBackup() { | |
| backupBtn.disabled = true; | |
| backupStatus.textContent = 'Backing up...'; | |
| backupStatus.className = 'backup-status'; | |
| try { | |
| const response = await fetch('/backup', { method: 'POST' }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| backupStatus.textContent = '✅ Backup successful!'; | |
| backupStatus.className = 'backup-status success'; | |
| addTerminalLine('[SYSTEM] GitHub backup completed successfully!', 'system'); | |
| } else { | |
| backupStatus.textContent = '❌ Backup failed: ' + (result.error || 'Unknown error'); | |
| backupStatus.className = 'backup-status error'; | |
| addTerminalLine('[ERROR] Backup failed: ' + (result.error || 'Unknown error'), 'error'); | |
| } | |
| } catch (e) { | |
| backupStatus.textContent = '❌ Network error'; | |
| backupStatus.className = 'backup-status error'; | |
| } | |
| setTimeout(() => { | |
| backupBtn.disabled = false; | |
| }, 5000); | |
| } | |
| async function checkBackupStatus() { | |
| backupStatus.textContent = 'Checking...'; | |
| try { | |
| const response = await fetch('/backup/status'); | |
| const result = await response.json(); | |
| if (result.error) { | |
| backupStatus.textContent = '❌ Error: ' + result.error; | |
| backupStatus.className = 'backup-status error'; | |
| } else if (result.has_changes) { | |
| backupStatus.textContent = '📝 Unsaved changes: ' + result.status.split('\\n').length + ' files'; | |
| backupStatus.className = 'backup-status'; | |
| } else { | |
| backupStatus.textContent = '✅ All changes synced'; | |
| backupStatus.className = 'backup-status success'; | |
| } | |
| } catch (e) { | |
| backupStatus.textContent = '❌ Network error'; | |
| backupStatus.className = 'backup-status error'; | |
| } | |
| } | |
| function connect() { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(protocol + '//' + window.location.host + '/ws'); | |
| ws.onopen = () => { | |
| console.log('WebSocket connected'); | |
| reconnectAttempts = 0; | |
| updateConnectionStatus('connected', 'Connected to server'); | |
| setTimeout(() => { connectionBar.style.opacity = '0'; }, 2000); | |
| }; | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'output') { | |
| addTerminalLine(data.data); | |
| } else if (data.type === 'stats') { | |
| currentStats = data; | |
| document.getElementById('cpuLoad').textContent = data.cpu.toFixed(1) + '%'; | |
| document.getElementById('memoryUsage').textContent = data.memory.toFixed(1) + '%'; | |
| const memTotal = (data.memoryTotal / 1024 / 1024 / 1024).toFixed(2); | |
| const memUsed = ((data.memoryTotal * data.memory / 100) / 1024 / 1024 / 1024).toFixed(2); | |
| document.getElementById('memorySub').textContent = memUsed + ' / ' + memTotal + ' GB'; | |
| document.getElementById('modalCpu').textContent = data.cpu.toFixed(1) + '%'; | |
| document.getElementById('modalMemory').textContent = data.memory.toFixed(1) + '%'; | |
| updateServerStatus(data.running, data.serverStartTime); | |
| if (data.players !== undefined) { | |
| document.getElementById('playerCount').textContent = data.players; | |
| document.getElementById('modalPlayers').textContent = data.players; | |
| } | |
| } else if (data.type === 'history') { | |
| data.logs.forEach(log => { | |
| addTerminalLine(log); | |
| }); | |
| } | |
| }; | |
| ws.onclose = () => { | |
| updateConnectionStatus('disconnected', 'Connection lost. Reconnecting...'); | |
| connectionBar.style.opacity = '1'; | |
| updateServerStatus(false); | |
| if (reconnectAttempts < maxReconnectAttempts) { | |
| reconnectAttempts++; | |
| setTimeout(connect, 3000); | |
| } | |
| }; | |
| ws.onerror = (error) => { | |
| console.error('WebSocket error:', error); | |
| }; | |
| } | |
| connect(); | |
| function sendInput() { | |
| const cmd = commandInput.value.trim(); | |
| if (cmd && ws && ws.readyState === WebSocket.OPEN) { | |
| if (typingPreviewLine && typingPreviewLine.parentNode) { | |
| typingPreviewLine.parentNode.removeChild(typingPreviewLine); | |
| typingPreviewLine = null; | |
| } | |
| ws.send(JSON.stringify({type: 'command', data: cmd})); | |
| commandInput.value = ''; | |
| } | |
| } | |
| function sendCmd(cmd) { | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({type: 'command', data: cmd})); | |
| } | |
| } | |
| commandInput.addEventListener('input', updateTypingPreview); | |
| commandInput.addEventListener('keyup', updateTypingPreview); | |
| commandInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| if (typingPreviewLine && typingPreviewLine.parentNode) { | |
| typingPreviewLine.parentNode.removeChild(typingPreviewLine); | |
| typingPreviewLine = null; | |
| } | |
| sendInput(); | |
| } | |
| }); | |
| commandInput.addEventListener('blur', () => { | |
| if (typingPreviewLine && typingPreviewLine.parentNode) { | |
| typingPreviewLine.parentNode.removeChild(typingPreviewLine); | |
| typingPreviewLine = null; | |
| } | |
| }); | |
| function switchTab(tab, element) { | |
| document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); | |
| element.classList.add('active'); | |
| } | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| function toggleSidebar() { | |
| sidebarOpen = !sidebarOpen; | |
| if (sidebarOpen) { | |
| sidebar.classList.add('active'); | |
| sidebarOverlay.classList.add('active'); | |
| } else { | |
| sidebar.classList.remove('active'); | |
| sidebarOverlay.classList.remove('active'); | |
| } | |
| } | |
| function toggleStatusModal() { | |
| statusModal.classList.toggle('active'); | |
| modalOverlay.classList.toggle('active'); | |
| document.getElementById('modalAddress').textContent = window.location.hostname; | |
| } | |
| document.addEventListener('click', (e) => { | |
| if (window.innerWidth <= 768 && sidebarOpen) { | |
| if (!sidebar.contains(e.target) && !e.target.closest('.sidebar-toggle')) { | |
| toggleSidebar(); | |
| } | |
| } | |
| }); | |
| window.addEventListener('orientationchange', () => { | |
| setTimeout(() => { | |
| terminal.scrollTop = terminal.scrollHeight; | |
| }, 300); | |
| }); | |
| let resizeTimer; | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimer); | |
| resizeTimer = setTimeout(() => { | |
| if (window.innerWidth > 768) { | |
| sidebar.classList.remove('active'); | |
| sidebarOverlay.classList.remove('active'); | |
| sidebarOpen = false; | |
| } | |
| }, 250); | |
| }); | |
| let lastTouchEnd = 0; | |
| document.addEventListener('touchend', (e) => { | |
| const now = Date.now(); | |
| if (now - lastTouchEnd <= 300) { | |
| e.preventDefault(); | |
| } | |
| lastTouchEnd = now; | |
| }, false); | |
| document.getElementById('serverAddress').textContent = window.location.hostname; | |
| addTerminalLine('[SYSTEM] Connecting to Minecraft server...', 'system'); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| @app.route('/') | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| @app.route('/health') | |
| def health(): | |
| return { | |
| 'status': 'running', | |
| 'minecraft': mc_process is not None and mc_process.poll() is None, | |
| 'timezone': 'Asia/Colombo', | |
| 'time': format_sl_time() | |
| } | |
| @sock.route('/ws') | |
| def websocket(ws): | |
| global server_start_time | |
| print("[WS] Client connected", flush=True) | |
| try: | |
| if logs_history: | |
| ws.send(json.dumps({ | |
| 'type': 'history', | |
| 'logs': logs_history[-100:] | |
| })) | |
| except Exception as e: | |
| print(f"[WS] History send error: {e}", flush=True) | |
| def send_stats(): | |
| while True: | |
| try: | |
| cpu = psutil.cpu_percent(interval=0.5) | |
| memory = psutil.virtual_memory() | |
| running = mc_process is not None and mc_process.poll() is None | |
| uptime_to_send = None | |
| if running and server_start_time: | |
| uptime_to_send = server_start_time | |
| ws.send(json.dumps({ | |
| 'type': 'stats', | |
| 'cpu': cpu, | |
| 'memory': memory.percent, | |
| 'memoryTotal': memory.total, | |
| 'running': running, | |
| 'serverStartTime': uptime_to_send, | |
| 'players': 0 | |
| })) | |
| except Exception as e: | |
| print(f"[WS] Stats error: {e}", flush=True) | |
| break | |
| time.sleep(2) | |
| stats_thread = threading.Thread(target=send_stats, daemon=True) | |
| stats_thread.start() | |
| def send_output(): | |
| while True: | |
| try: | |
| line = output_queue.get(timeout=1) | |
| try: | |
| ws.send(json.dumps({'type': 'output', 'data': line})) | |
| except: | |
| break | |
| except queue.Empty: | |
| continue | |
| except: | |
| break | |
| output_thread = threading.Thread(target=send_output, daemon=True) | |
| output_thread.start() | |
| while True: | |
| try: | |
| data = ws.receive(timeout=5) | |
| if data: | |
| msg = json.loads(data) | |
| if msg.get('type') == 'command': | |
| cmd = msg.get('data', '').strip() | |
| if cmd: | |
| print(f"[WS] Command: {cmd}", flush=True) | |
| # Handle web UI control commands | |
| if cmd == 'restart': | |
| add_log_line("[SYSTEM] Restart requested from Web UI") | |
| # Trigger restart via signal file | |
| with open(RESTART_SIGNAL_FILE, 'w') as f: | |
| f.write('web_restart') | |
| # Also send stop to gracefully shutdown | |
| if mc_process and mc_process.poll() is None: | |
| try: | |
| mc_process.stdin.write("stop\n") | |
| mc_process.stdin.flush() | |
| except: | |
| pass | |
| elif cmd == 'stop': | |
| add_log_line("[SYSTEM] Stop requested from Web UI") | |
| if mc_process and mc_process.poll() is None: | |
| try: | |
| mc_process.stdin.write("stop\n") | |
| mc_process.stdin.flush() | |
| except: | |
| pass | |
| elif cmd == 'start': | |
| add_log_line("[SYSTEM] Start requested from Web UI") | |
| if mc_process is None or mc_process.poll() is not None: | |
| start_minecraft() | |
| else: | |
| command_queue.put(cmd) | |
| except Exception as e: | |
| if "Connection closed" in str(e): | |
| break | |
| continue | |
| print("[WS] Client disconnected", flush=True) | |
| def cleanup(signum=None, frame=None): | |
| print("\n[MAIN] Shutting down...", flush=True) | |
| save_logs_to_file() | |
| if mc_process: | |
| try: | |
| mc_process.stdin.write("stop\n") | |
| mc_process.stdin.flush() | |
| mc_process.wait(timeout=30) | |
| except: | |
| try: | |
| os.killpg(os.getpgid(mc_process.pid), signal.SIGTERM) | |
| except: | |
| pass | |
| sys.exit(0) | |
| signal.signal(signal.SIGTERM, cleanup) | |
| signal.signal(signal.SIGINT, cleanup) | |
| if __name__ == '__main__': | |
| load_logs_from_file() | |
| load_uptime_from_file() | |
| print(f"[MAIN] Starting web server... Timezone: {SL_TZ}", flush=True) | |
| mc_thread = threading.Thread(target=start_minecraft, daemon=True) | |
| mc_thread.start() | |
| # Start restart monitor thread | |
| restart_monitor = threading.Thread(target=monitor_restart_signal, daemon=True) | |
| restart_monitor.start() | |
| def github_push_loop(): | |
| while True: | |
| time.sleep(300) | |
| push_logs_to_github() | |
| github_thread = threading.Thread(target=github_push_loop, daemon=True) | |
| github_thread.start() | |
| from werkzeug.serving import run_simple | |
| run_simple('0.0.0.0', 7860, app, threaded=True, use_reloader=False) | |
| PYTHON_SCRIPT | |
| echo "🌐 Starting Web Terminal Server..." | |
| python3 /app/web_terminal.py 2>&1 & | |
| sleep 5 | |
| if pgrep -f "web_terminal.py" > /dev/null; then | |
| echo "✅ Web Terminal Server is running on port 7860" | |
| else | |
| echo "❌ Web Terminal Server failed to start" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "==========================================" | |
| echo "✅ Minecraft Server with GitHub Backup is LIVE!" | |
| echo "🌐 Access at: https://your-space.hf.space" | |
| echo "📱 Mobile optimized interface enabled" | |
| echo "💾 Logs are now persistent across refreshes" | |
| echo "⏰ Uptime tracking fixed" | |
| echo "⌨️ Real-time command typing preview enabled" | |
| echo "📊 Status modal with heartbeat icon added" | |
| echo "☁️ GitHub Auto-backup every 5 minutes" | |
| echo "🔧 Manual backup button added to sidebar" | |
| echo "🔄 Pull-before-push conflict resolution" | |
| echo "🔄 RESTART COMMAND FIXED - Python handles restart!" | |
| echo "🌍 Timezone: Asia/Colombo (UTC+5:30) - Sri Lankan Time" | |
| echo "==========================================" | |
| # Keep container alive | |
| while true; do | |
| sleep 30 | |
| if ! pgrep -f "server.jar" > /dev/null; then | |
| echo "⚠️ Minecraft server not running!" | |
| fi | |
| if ! pgrep -f "web_terminal.py" > /dev/null; then | |
| echo "❌ Web Terminal crashed! Restarting..." | |
| python3 /app/web_terminal.py 2>&1 & | |
| sleep 5 | |
| fi | |
| done | |
| EOF | |
| RUN chmod +x start.sh | |
| EXPOSE 7860 25565 25575 | |
| CMD ["./start.sh"] | |