wpmfun / web_terminal.py
srinetwork's picture
Create web_terminal.py
b513a41 verified
Raw History Blame Contribute Delete
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>"""
@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}
@app.route('/api/files')
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
@sock.route('/ws')
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)