Download web_terminal.py from wpms/wpmfun: direct link, hf CLI and curl.
- Browser
- Download file 38.1 kB
-
https://huggingface.co/spaces/wpms/wpmfun/resolve/main/web_terminal.py
- Command line
-
hf download hf://spaces/wpms/wpmfun/web_terminal.py
-
curl -L -o web_terminal.py https://huggingface.co/spaces/wpms/wpmfun/resolve/main/web_terminal.py
38.1 kB
| import os | |
| import sys | |
| import subprocess | |
| import json | |
| import threading | |
| import time | |
| import queue | |
| import signal | |
| from flask import Flask, render_template_string, request, jsonify | |
| from flask_sock import Sock | |
| import psutil | |
| app = Flask(__name__) | |
| sock = Sock(app) | |
| mc_process = None | |
| output_queue = queue.Queue() | |
| command_queue = queue.Queue() | |
| server_ready = threading.Event() | |
| def start_minecraft(): | |
| global mc_process | |
| mc_dir = "/app/mcserver" | |
| print("[MC] Starting Minecraft server...", flush=True) | |
| 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) | |
| def read_output(): | |
| try: | |
| for line in mc_process.stdout: | |
| output_queue.put(line) | |
| if "Done" in line and "For help, type" in line: | |
| server_ready.set() | |
| print("[MC] Server is ready!", flush=True) | |
| except Exception as e: | |
| print(f"[MC] Output error: {e}", flush=True) | |
| 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) | |
| 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() | |
| 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; } | |
| :root { | |
| --bg-primary: #0f1419; --bg-secondary: #161b22; --bg-tertiary: #1c2128; | |
| --bg-card: #21262d; --border-color: #30363d; | |
| --text-primary: #f0f6fc; --text-secondary: #c9d1d9; --text-muted: #8b949e; | |
| --accent-blue: #58a6ff; --accent-green: #3fb950; --accent-red: #f85149; | |
| --accent-yellow: #d29922; --accent-purple: #a371f7; --accent-cyan: #39c5cf; | |
| --accent-orange: #fb8500; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--bg-primary); color: var(--text-primary); | |
| height: 100vh; overflow: hidden; font-size: 14px; | |
| } | |
| .top-nav { | |
| background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); | |
| padding: 0 15px; display: flex; align-items: center; height: 50px; | |
| } | |
| .nav-tabs { display: flex; height: 100%; } | |
| .nav-tab { | |
| padding: 0 18px; display: flex; align-items: center; gap: 8px; | |
| color: var(--text-muted); font-size: 14px; font-weight: 500; | |
| border-bottom: 2px solid transparent; cursor: pointer; text-decoration: none; | |
| } | |
| .nav-tab:hover { color: var(--text-primary); background: rgba(255,255,255,0.03); } | |
| .nav-tab.active { color: var(--accent-blue); border-bottom-color: var(--accent-blue); background: rgba(88,166,255,0.08); } | |
| .nav-right { margin-left: auto; display: flex; gap: 5px; } | |
| .nav-btn { background: transparent; border: none; color: var(--text-muted); padding: 8px 10px; cursor: pointer; border-radius: 6px; } | |
| .nav-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); } | |
| .main-container { display: flex; height: calc(100vh - 50px); } | |
| .content-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; } | |
| .console-header { | |
| background: var(--bg-secondary); padding: 12px 15px; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; | |
| } | |
| .server-title { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 10px; } | |
| .server-title i { color: var(--accent-blue); font-size: 18px; } | |
| .control-buttons { display: flex; gap: 6px; } | |
| .btn { padding: 8px 14px; border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; } | |
| .btn-primary { background: var(--accent-blue); color: #fff; } | |
| .btn-secondary { background: var(--bg-card); color: var(--text-secondary); border: 1px solid var(--border-color); } | |
| .btn-danger { background: var(--accent-red); color: #fff; } | |
| .btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .terminal-container { flex: 1; background: var(--bg-primary); overflow: hidden; display: flex; flex-direction: column; } | |
| .terminal { | |
| flex: 1; padding: 12px 15px; font-family: 'SFMono-Regular', Consolas, monospace; | |
| font-size: 13px; line-height: 1.6; color: var(--text-secondary); | |
| overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; | |
| } | |
| .log-line { margin: 1px 0; padding: 2px 0; border-radius: 3px; } | |
| .log-line:hover { background: rgba(255,255,255,0.03); } | |
| .log-debug { color: #6e7681; } .log-info { color: var(--accent-blue); } | |
| .log-warn { color: var(--accent-yellow); } .log-error { color: var(--accent-red); font-weight: 500; } | |
| .log-fatal { color: var(--accent-red); background: rgba(248,81,73,0.1); font-weight: 600; } | |
| .log-success { color: var(--accent-green); } .log-chat { color: var(--accent-purple); } | |
| .log-join { color: var(--accent-green); } .log-leave { color: var(--accent-orange); } | |
| .log-time { color: #6e7681; font-size: 12px; } | |
| .log-level { display: inline-block; padding: 0 6px; border-radius: 3px; font-size: 11px; font-weight: 700; margin-right: 6px; } | |
| .level-info { background: rgba(88,166,255,0.2); color: var(--accent-blue); } | |
| .level-warn { background: rgba(210,153,34,0.2); color: var(--accent-yellow); } | |
| .level-error { background: rgba(248,81,73,0.2); color: var(--accent-red); } | |
| .command-area { | |
| background: var(--bg-secondary); border-top: 1px solid var(--border-color); | |
| padding: 12px 15px; display: flex; gap: 10px; | |
| } | |
| .command-input-wrapper { | |
| flex: 1; display: flex; align-items: center; background: var(--bg-primary); | |
| border: 1px solid var(--border-color); border-radius: 8px; padding: 0 12px; | |
| } | |
| .command-input-wrapper:focus-within { border-color: var(--accent-blue); } | |
| .command-input-wrapper i { color: var(--text-muted); margin-right: 10px; } | |
| #commandInput { flex: 1; background: transparent; border: none; color: var(--text-primary); padding: 12px 0; font-size: 14px; outline: none; } | |
| .send-btn { background: var(--accent-green); color: #fff; border: none; padding: 12px 20px; border-radius: 8px; cursor: pointer; font-weight: 600; } | |
| .send-btn:disabled { opacity: 0.4; } | |
| .sidebar { | |
| width: 280px; background: var(--bg-secondary); border-left: 1px solid var(--border-color); | |
| padding: 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; | |
| } | |
| .stat-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 10px; padding: 14px; display: flex; align-items: center; gap: 12px; } | |
| .stat-card:hover { border-color: var(--accent-blue); } | |
| .stat-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; } | |
| .stat-icon.blue { background: rgba(88,166,255,0.15); color: var(--accent-blue); } | |
| .stat-icon.green { background: rgba(63,185,80,0.15); color: var(--accent-green); } | |
| .stat-icon.red { background: rgba(248,81,73,0.15); color: var(--accent-red); } | |
| .stat-icon.yellow { background: rgba(210,153,34,0.15); color: var(--accent-yellow); } | |
| .stat-icon.purple { background: rgba(163,113,247,0.15); color: var(--accent-purple); } | |
| .stat-icon.cyan { background: rgba(57,197,207,0.15); color: var(--accent-cyan); } | |
| .stat-info { flex: 1; } | |
| .stat-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 3px; } | |
| .stat-value { font-size: 16px; font-weight: 700; } | |
| .stat-sub { font-size: 11px; color: var(--text-muted); } | |
| .status-indicator { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; text-transform: uppercase; } | |
| .status-online { background: rgba(63,185,80,0.2); color: var(--accent-green); } | |
| .status-offline { background: rgba(248,81,73,0.2); color: var(--accent-red); } | |
| .pulse { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } } | |
| .connection-bar { position: absolute; top: 0; left: 0; right: 0; padding: 8px 15px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 8px; z-index: 50; } | |
| .connection-bar.connected { background: rgba(63,185,80,0.15); color: var(--accent-green); } | |
| .connection-bar.disconnected { background: rgba(248,81,73,0.15); color: var(--accent-red); } | |
| .connection-bar.connecting { background: rgba(210,153,34,0.15); color: var(--accent-yellow); } | |
| .file-manager { flex: 1; display: none; flex-direction: column; background: var(--bg-primary); } | |
| .file-manager.active { display: flex; } | |
| .file-header { background: var(--bg-secondary); padding: 12px 15px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 10px; } | |
| .breadcrumb { flex: 1; display: flex; align-items: center; gap: 5px; font-size: 14px; overflow-x: auto; } | |
| .breadcrumb-item { white-space: nowrap; cursor: pointer; } | |
| .breadcrumb-item:hover { color: var(--accent-blue); } | |
| .breadcrumb-separator { color: var(--text-muted); } | |
| .breadcrumb-item.active { color: var(--accent-blue); font-weight: 600; } | |
| .file-actions { display: flex; gap: 8px; } | |
| .file-btn { background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-secondary); padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 6px; } | |
| .file-btn:hover { background: var(--border-color); color: var(--text-primary); } | |
| .file-list { flex: 1; overflow-y: auto; padding: 10px; } | |
| .file-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; cursor: pointer; margin-bottom: 2px; } | |
| .file-item:hover { background: var(--bg-tertiary); } | |
| .file-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; } | |
| .file-icon.folder { background: rgba(88,166,255,0.15); color: var(--accent-blue); } | |
| .file-icon.file { background: var(--bg-tertiary); color: var(--text-muted); } | |
| .file-icon.image { background: rgba(163,113,247,0.15); color: var(--accent-purple); } | |
| .file-icon.code { background: rgba(57,197,207,0.15); color: var(--accent-cyan); } | |
| .file-icon.config { background: rgba(210,153,34,0.15); color: var(--accent-yellow); } | |
| .file-info { flex: 1; } | |
| .file-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .file-meta { font-size: 12px; color: var(--text-muted); } | |
| .file-size { font-size: 12px; color: var(--text-muted); white-space: nowrap; } | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: var(--bg-primary); } | |
| ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } | |
| @media (max-width: 768px) { | |
| .top-nav { height: 45px; } | |
| .nav-tab { padding: 0 12px; font-size: 13px; } | |
| .main-container { flex-direction: column; height: calc(100vh - 45px); } | |
| .sidebar { width: 100%; border-left: none; border-top: 1px solid var(--border-color); padding: 10px; flex-direction: row; overflow-x: auto; gap: 8px; order: 2; min-height: 80px; } | |
| .stat-card { min-width: 140px; padding: 10px; flex-direction: column; text-align: center; } | |
| .stat-icon { width: 32px; height: 32px; } | |
| .stat-sub { display: none; } | |
| .content-area { order: 1; } | |
| .console-header { padding: 10px; } | |
| .server-title { font-size: 14px; } | |
| .btn { padding: 6px 10px; font-size: 12px; } | |
| .terminal { font-size: 12px; padding: 10px; } | |
| .command-area { padding: 10px; } | |
| #commandInput { padding: 10px 0; font-size: 13px; } | |
| .send-btn { padding: 10px 14px; font-size: 13px; } | |
| } | |
| @media (max-width: 480px) { | |
| .nav-tab span { display: none; } | |
| .nav-tab { padding: 0 15px; } | |
| .btn span { display: none; } | |
| .btn { padding: 8px; } | |
| .stat-card { min-width: 100px; } | |
| .stat-icon { width: 28px; height: 28px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="top-nav"> | |
| <div class="nav-tabs"> | |
| <a class="nav-tab active" onclick="switchTab('console', this)" id="consoleTab"> | |
| <i class="fas fa-terminal"></i><span>Console</span> | |
| </a> | |
| <a class="nav-tab" onclick="switchTab('files', this)" id="filesTab"> | |
| <i class="fas fa-folder"></i><span>Files</span> | |
| </a> | |
| </div> | |
| <div class="nav-right"> | |
| <button class="nav-btn" onclick="toggleSettings()"><i class="fas fa-cog"></i></button> | |
| <button class="nav-btn" onclick="toggleFullscreen()"><i class="fas fa-expand"></i></button> | |
| </div> | |
| </nav> | |
| <div class="main-container"> | |
| <div class="content-area"> | |
| <div class="console-section" id="consoleSection"> | |
| <div class="connection-bar connecting" id="connectionBar"> | |
| <i class="fas fa-circle-notch fa-spin"></i><span>Connecting...</span> | |
| </div> | |
| <div class="console-header"> | |
| <div class="server-title"> | |
| <i class="fas fa-cube"></i> | |
| <span>Minecraft Server</span> | |
| <span class="status-indicator status-offline" id="serverStatus"> | |
| <span class="pulse"></span><span>Offline</span> | |
| </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> | |
| <div class="file-manager" id="fileSection"> | |
| <div class="file-header"> | |
| <div class="breadcrumb" id="breadcrumb"> | |
| <span class="breadcrumb-item active" onclick="goHome()"><i class="fas fa-home"></i></span> | |
| <span class="breadcrumb-separator">/</span> | |
| <span class="breadcrumb-item active">mcserver</span> | |
| </div> | |
| <div class="file-actions"> | |
| <button class="file-btn" onclick="loadFiles()"><i class="fas fa-sync-alt"></i><span>Refresh</span></button> | |
| <button class="file-btn" onclick="goBack()"><i class="fas fa-arrow-up"></i><span>Up</span></button> | |
| </div> | |
| </div> | |
| <div class="file-list" id="fileList"> | |
| <div style="text-align: center; padding: 40px; color: var(--text-muted);"> | |
| <i class="fas fa-folder-open" style="font-size: 48px; margin-bottom: 15px; opacity: 0.5;"></i> | |
| <p>Loading files...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="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" style="font-size: 13px;">--</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">--</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">System usage</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 size</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</div> | |
| </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'); | |
| let ws = null, reconnectAttempts = 0, maxReconnectAttempts = 10; | |
| let serverStartTime = null, isServerRunning = false, currentPath = '/app/mcserver'; | |
| function getLogClass(text) { | |
| if (text.includes('ERROR') || text.includes('Exception')) return 'log-error'; | |
| if (text.includes('WARN')) return 'log-warn'; | |
| if (text.includes('FATAL')) return 'log-fatal'; | |
| if (text.includes('joined') || text.includes('left')) return 'log-join'; | |
| if (text.includes('[CHAT]')) return 'log-chat'; | |
| if (text.includes('Done') || text.includes('Starting')) return 'log-success'; | |
| if (text.includes('INFO')) return 'log-info'; | |
| return ''; | |
| } | |
| 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>'; | |
| } | |
| function updateServerStatus(running) { | |
| isServerRunning = running; | |
| const statusEl = document.getElementById('serverStatus'); | |
| if (running) { | |
| statusEl.className = 'status-indicator status-online'; | |
| statusEl.innerHTML = '<span class="pulse"></span><span>Online</span>'; | |
| startBtn.disabled = true; restartBtn.disabled = false; stopBtn.disabled = false; | |
| commandInput.disabled = false; sendBtn.disabled = false; | |
| if (!serverStartTime) serverStartTime = Date.now(); | |
| } else { | |
| statusEl.className = 'status-indicator status-offline'; | |
| statusEl.innerHTML = '<span class="pulse"></span><span>Offline</span>'; | |
| startBtn.disabled = false; restartBtn.disabled = true; stopBtn.disabled = true; | |
| commandInput.disabled = true; sendBtn.disabled = true; | |
| serverStartTime = null; | |
| } | |
| } | |
| function addTerminalLine(text) { | |
| const line = document.createElement('div'); | |
| line.className = 'log-line ' + getLogClass(text); | |
| let formatted = text | |
| .replace(/(\[\d{2}:\d{2}:\d{2}\])/g, '<span class="log-time">$1</span>') | |
| .replace(/\[Server thread\/INFO\]:/g, '<span class="log-level level-info">INFO</span>') | |
| .replace(/\[Server thread\/WARN\]:/g, '<span class="log-level level-warn">WARN</span>') | |
| .replace(/\[Server thread\/ERROR\]:/g, '<span class="log-level level-error">ERROR</span>') | |
| .replace(/\[Server thread\/FATAL\]:/g, '<span class="log-level level-error">FATAL</span>'); | |
| line.innerHTML = formatted; | |
| terminal.appendChild(line); | |
| terminal.scrollTop = terminal.scrollHeight; | |
| while (terminal.children.length > 500) terminal.removeChild(terminal.firstChild); | |
| } | |
| function formatUptime(ms) { | |
| const seconds = Math.floor(ms / 1000); | |
| const minutes = Math.floor(seconds / 60); | |
| const hours = Math.floor(minutes / 60); | |
| const days = Math.floor(hours / 24); | |
| if (days > 0) return days + 'd ' + (hours % 24) + 'h'; | |
| if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm'; | |
| if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's'; | |
| return seconds + 's'; | |
| } | |
| function updateUptime() { | |
| if (serverStartTime && isServerRunning) { | |
| document.getElementById('uptime').textContent = formatUptime(Date.now() - serverStartTime); | |
| document.getElementById('uptimeSub').textContent = 'Running'; | |
| } else { | |
| document.getElementById('uptime').textContent = '--'; | |
| document.getElementById('uptimeSub').textContent = 'Not running'; | |
| } | |
| } | |
| setInterval(updateUptime, 1000); | |
| function connect() { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(protocol + '//' + window.location.host + '/ws'); | |
| ws.onopen = () => { | |
| reconnectAttempts = 0; | |
| updateConnectionStatus('connected', 'Connected'); | |
| setTimeout(() => { connectionBar.style.display = 'none'; }, 2000); | |
| }; | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'output') addTerminalLine(data.data); | |
| else if (data.type === 'stats') { | |
| 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'; | |
| updateServerStatus(data.running); | |
| if (data.players !== undefined) document.getElementById('playerCount').textContent = data.players; | |
| if (data.disk !== undefined) document.getElementById('diskUsage').textContent = (data.disk / 1024 / 1024).toFixed(2) + ' MB'; | |
| } | |
| }; | |
| ws.onclose = () => { | |
| connectionBar.style.display = 'flex'; | |
| updateConnectionStatus('disconnected', 'Reconnecting...'); | |
| updateServerStatus(false); | |
| if (reconnectAttempts < maxReconnectAttempts) { | |
| reconnectAttempts++; | |
| setTimeout(connect, 3000); | |
| } | |
| }; | |
| } | |
| connect(); | |
| function sendInput() { | |
| const cmd = commandInput.value.trim(); | |
| if (cmd && ws && ws.readyState === WebSocket.OPEN) { | |
| 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('keypress', (e) => { if (e.key === 'Enter') sendInput(); }); | |
| function switchTab(tab, element) { | |
| document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); | |
| element.classList.add('active'); | |
| if (tab === 'console') { | |
| document.getElementById('consoleSection').style.display = 'flex'; | |
| document.getElementById('fileSection').classList.remove('active'); | |
| } else { | |
| document.getElementById('consoleSection').style.display = 'none'; | |
| document.getElementById('fileSection').classList.add('active'); | |
| loadFiles(); | |
| } | |
| } | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) document.documentElement.requestFullscreen(); | |
| else document.exitFullscreen(); | |
| } | |
| function toggleSettings() { alert('Settings coming soon!'); } | |
| function getFileIconClass(filename, isDir) { | |
| if (isDir) return 'folder'; | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| if (['png','jpg','jpeg','gif','bmp','ico'].includes(ext)) return 'image'; | |
| if (['js','py','java','cpp','c','h','php','rb','go','rs'].includes(ext)) return 'code'; | |
| if (['json','xml','yml','yaml','properties','conf','cfg','ini'].includes(ext)) return 'config'; | |
| return 'file'; | |
| } | |
| function getFileIcon(filename, isDir) { | |
| if (isDir) return 'fa-folder'; | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| const icons = { 'png': 'fa-file-image', 'jpg': 'fa-file-image', 'jpeg': 'fa-file-image', 'json': 'fa-file-code', 'xml': 'fa-file-code', 'yml': 'fa-file-code', 'properties': 'fa-file-cog', 'conf': 'fa-file-cog', 'cfg': 'fa-file-cog', 'txt': 'fa-file-alt', 'md': 'fa-file-alt', 'log': 'fa-file-alt', 'jar': 'fa-file-archive', 'zip': 'fa-file-archive', 'db': 'fa-database', 'sql': 'fa-database' }; | |
| return icons[ext] || 'fa-file'; | |
| } | |
| function formatSize(bytes) { | |
| if (bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| async function loadFiles() { | |
| const fileList = document.getElementById('fileList'); | |
| fileList.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);"><i class="fas fa-spinner fa-spin" style="font-size: 32px; margin-bottom: 15px;"></i><p>Loading...</p></div>'; | |
| try { | |
| const response = await fetch('/api/files?path=' + encodeURIComponent(currentPath)); | |
| const files = await response.json(); | |
| if (files.length === 0) { | |
| fileList.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);"><i class="fas fa-folder-open" style="font-size: 48px; margin-bottom: 15px; opacity: 0.5;"></i><p>Empty folder</p></div>'; | |
| return; | |
| } | |
| fileList.innerHTML = ''; | |
| files.sort((a, b) => { if (a.is_dir === b.is_dir) return a.name.localeCompare(b.name); return b.is_dir - a.is_dir; }); | |
| files.forEach(file => { | |
| const item = document.createElement('div'); | |
| item.className = 'file-item'; | |
| const iconClass = getFileIconClass(file.name, file.is_dir); | |
| const icon = getFileIcon(file.name, file.is_dir); | |
| item.innerHTML = '<div class="file-icon ' + iconClass + '"><i class="fas ' + icon + '"></i></div><div class="file-info"><div class="file-name">' + file.name + '</div><div class="file-meta">' + (file.is_dir ? 'Folder' : formatSize(file.size)) + '</div></div><div class="file-size">' + (file.is_dir ? '' : formatSize(file.size)) + '</div>'; | |
| if (file.is_dir) item.onclick = () => { currentPath = file.path; updateBreadcrumb(); loadFiles(); }; | |
| fileList.appendChild(item); | |
| }); | |
| } catch (error) { | |
| fileList.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);"><i class="fas fa-exclamation-triangle" style="font-size: 32px; margin-bottom: 15px;"></i><p>Error loading files</p></div>'; | |
| } | |
| } | |
| function updateBreadcrumb() { | |
| const parts = currentPath.replace('/app/mcserver', '').split('/').filter(p => p); | |
| let html = '<span class="breadcrumb-item active" onclick="goHome()"><i class="fas fa-home"></i></span>'; | |
| if (parts.length === 0) { | |
| html += '<span class="breadcrumb-separator">/</span><span class="breadcrumb-item active">mcserver</span>'; | |
| } else { | |
| let path = '/app/mcserver'; | |
| html += '<span class="breadcrumb-separator">/</span><span class="breadcrumb-item" onclick="goHome()">mcserver</span>'; | |
| parts.forEach((part, i) => { | |
| path += '/' + part; | |
| const isLast = i === parts.length - 1; | |
| html += '<span class="breadcrumb-separator">/</span><span class="breadcrumb-item ' + (isLast ? 'active' : '') + '" ' + (!isLast ? 'onclick="navigateTo(\'' + path + '\')"' : '') + '>' + part + '</span>'; | |
| }); | |
| } | |
| document.getElementById('breadcrumb').innerHTML = html; | |
| } | |
| function goHome() { currentPath = '/app/mcserver'; updateBreadcrumb(); loadFiles(); } | |
| function navigateTo(path) { currentPath = path; updateBreadcrumb(); loadFiles(); } | |
| function goBack() { | |
| if (currentPath === '/app/mcserver') return; | |
| const parts = currentPath.split('/'); | |
| parts.pop(); | |
| currentPath = parts.join('/') || '/app/mcserver'; | |
| updateBreadcrumb(); | |
| loadFiles(); | |
| } | |
| document.getElementById('serverAddress').textContent = window.location.hostname; | |
| addTerminalLine('Connecting to Minecraft server...'); | |
| </script> | |
| </body> | |
| </html>""" | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def health(): | |
| return {'status': 'running', 'minecraft': mc_process is not None and mc_process.poll() is None} | |
| def get_files(): | |
| path = request.args.get('path', '/app/mcserver') | |
| if not path.startswith('/app/mcserver'): | |
| path = '/app/mcserver' | |
| try: | |
| files = [] | |
| for item in sorted(os.listdir(path)): | |
| item_path = os.path.join(path, item) | |
| is_dir = os.path.isdir(item_path) | |
| size = os.path.getsize(item_path) if not is_dir else 0 | |
| files.append({'name': item, 'is_dir': is_dir, 'size': size, 'path': item_path}) | |
| return jsonify(files) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| def websocket(ws): | |
| print("[WS] Client connected", flush=True) | |
| history = [] | |
| temp_list = [] | |
| while not output_queue.empty() and len(temp_list) < 100: | |
| try: | |
| line = output_queue.get_nowait() | |
| temp_list.append(line) | |
| except: | |
| break | |
| for line in temp_list: | |
| output_queue.put(line) | |
| history.append(line) | |
| for line in history[-50:]: | |
| try: | |
| ws.send(json.dumps({'type': 'output', 'data': line})) | |
| except: | |
| break | |
| 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 | |
| disk_size = 0 | |
| try: | |
| for dirpath, dirnames, filenames in os.walk('/app/mcserver'): | |
| for f in filenames: | |
| fp = os.path.join(dirpath, f) | |
| if os.path.exists(fp): | |
| disk_size += os.path.getsize(fp) | |
| except: | |
| pass | |
| ws.send(json.dumps({ | |
| 'type': 'stats', | |
| 'cpu': cpu, | |
| 'memory': memory.percent, | |
| 'memoryTotal': memory.total, | |
| 'running': running, | |
| 'players': 0, | |
| 'disk': disk_size | |
| })) | |
| except Exception as e: | |
| print(f"[WS] Stats error: {e}", flush=True) | |
| break | |
| time.sleep(2) | |
| threading.Thread(target=send_stats, daemon=True).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 | |
| threading.Thread(target=send_output, daemon=True).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) | |
| 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) | |
| 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__': | |
| print("[MAIN] Starting web server...", flush=True) | |
| mc_thread = threading.Thread(target=start_minecraft, daemon=True) | |
| mc_thread.start() | |
| from werkzeug.serving import run_simple | |
| run_simple('0.0.0.0', 7860, app, threaded=True, use_reloader=False) | |