wpmss / Dockerfile
srinetwork's picture
Update Dockerfile
4f78e38 verified
Raw History Blame Contribute Delete
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"]