freeroot / Dockerfile
wpms's picture
Update Dockerfile
e6f35f3 verified
Raw History Blame Contribute Delete
69.7 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 FreeRoot 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 FreeRoot server from GitHub..."
rm -rf mcserver
git clone https://$GITHUB_TOKEN@github.com/Walukapah/Freeroot.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
# FreeRoot 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 "freeroot@huggingface.co"
git config --global user.name "FreeRoot Bot"
git config --global init.defaultBranch main
# Ensure remote URL has token
git remote set-url origin https://$GITHUB_TOKEN@github.com/Walukapah/Freeroot.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 300
/app/mcserver/backup.sh 2>&1 || true
done
) &
echo "✅ Auto-backup enabled (every 5 minutes)"
echo ""
# ==========================================
# WEB TERMINAL SERVER (PTY SUPPORT FOR INTERACTIVE SHELL)
# ==========================================
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
import pty
import select
import fcntl
import struct
import termios
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)
# SSH Process variables
ssh_process = None
ssh_master = None
ssh_fd = 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/Freeroot"
# 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 set_terminal_size(fd, rows=30, cols=120):
"""Set PTY terminal size"""
try:
size = struct.pack('HHHH', rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, size)
except Exception as e:
print(f"[PTY] Error setting terminal size: {e}", flush=True)
def start_freeroot():
"""Start the Freeroot SSH server with PTY"""
global ssh_process, ssh_master, ssh_fd, 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("[FREEROOT] Starting Freeroot server...", flush=True)
add_log_line("[SYSTEM] Starting Freeroot server...")
server_start_time = time.time()
save_uptime_to_file()
# Create PTY for interactive shell
ssh_master, ssh_fd = pty.openpty()
# Set terminal size
set_terminal_size(ssh_master, rows=40, cols=150)
# Start server.jar with PTY
ssh_process = subprocess.Popen(
["java", "-jar", "server.jar"],
cwd=mc_dir,
stdin=ssh_fd,
stdout=ssh_fd,
stderr=ssh_fd,
preexec_fn=os.setsid
)
os.close(ssh_fd)
ssh_fd = None
print(f"[FREEROOT] Server PID: {ssh_process.pid}", flush=True)
add_log_line(f"[SYSTEM] Freeroot server started with PID: {ssh_process.pid}")
def read_output():
"""Read output from PTY master"""
try:
while True:
ready, _, _ = select.select([ssh_master], [], [], 0.1)
if ready:
try:
data = os.read(ssh_master, 4096)
if not data:
break
# Decode with error handling for ANSI codes
try:
line = data.decode('utf-8', errors='replace')
except:
line = data.decode('latin-1', errors='replace')
# Split by newlines and add each line
for l in line.split('\n'):
l = l.rstrip('\r').strip()
if l:
add_log_line(l)
except OSError:
break
else:
# Check if process is still running
if ssh_process.poll() is not None:
break
time.sleep(0.05)
except Exception as e:
print(f"[FREEROOT] 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():
"""Send commands to PTY"""
while True:
try:
cmd = command_queue.get(timeout=1)
if ssh_process and ssh_process.poll() is None and ssh_master is not None:
try:
# Send command with newline
cmd_bytes = (cmd + "\n").encode('utf-8')
os.write(ssh_master, cmd_bytes)
print(f"[FREEROOT] Command sent: {cmd}", flush=True)
add_log_line(f"> {cmd}")
except Exception as e:
print(f"[FREEROOT] Command error: {e}", flush=True)
except queue.Empty:
continue
except Exception as e:
print(f"[FREEROOT] Processor error: {e}", flush=True)
break
threading.Thread(target=process_commands, daemon=True).start()
def stop_freeroot():
"""Gracefully stop the Freeroot server"""
global ssh_process, ssh_master
if ssh_process and ssh_process.poll() is None:
print("[FREEROOT] Stopping server...", flush=True)
add_log_line("[SYSTEM] Stopping Freeroot server...")
try:
# Send exit command first
if ssh_master is not None:
try:
os.write(ssh_master, b"exit\n")
time.sleep(1)
except:
pass
ssh_process.terminate()
ssh_process.wait(timeout=10)
print("[FREEROOT] Server stopped gracefully", flush=True)
add_log_line("[SYSTEM] Server stopped gracefully")
except:
try:
ssh_process.kill()
ssh_process.wait(timeout=5)
except:
pass
ssh_process = None
if ssh_master is not None:
try:
os.close(ssh_master)
except:
pass
ssh_master = None
def restart_freeroot():
"""Restart the Freeroot server"""
print("[RESTART] Restarting Freeroot server...", flush=True)
add_log_line("[SYSTEM] Restarting Freeroot server...")
stop_freeroot()
time.sleep(3)
start_freeroot()
print("[RESTART] Server restart complete", flush=True)
add_log_line("[SYSTEM] Server restart complete")
def monitor_restart_signal():
"""Monitor for restart signals"""
while True:
try:
if os.path.exists(RESTART_SIGNAL_FILE):
print("[MONITOR] Restart signal detected!", flush=True)
os.remove(RESTART_SIGNAL_FILE)
restart_freeroot()
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>FreeRoot 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-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-container {
display: flex;
height: calc(100vh - 50px);
flex-direction: row;
}
.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-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-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 {
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-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; } }
::-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-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); }
.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 {
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 {
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; }
@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>
<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>
<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">FreeRoot 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 Sessions</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>
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<div class="main-container">
<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>FreeRoot 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>
<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 2222</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">Sessions</div>
<div class="stat-value" id="playerCount">--</div>
<div class="stat-sub">Active SSH sessions</div>
</div>
</div>
<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 stripAnsi(str) {
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1B\[[0-9;]*m/g, '');
}
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 = stripAnsi(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);
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 FreeRoot server...', 'system');
</script>
</body>
</html>
"""
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/health')
def health():
return {
'status': 'running',
'freeroot': ssh_process is not None and ssh_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 = ssh_process is not None and ssh_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 exit to gracefully shutdown
if ssh_process and ssh_process.poll() is None:
try:
if ssh_master is not None:
os.write(ssh_master, b"exit\n")
except:
pass
elif cmd == 'stop':
add_log_line("[SYSTEM] Stop requested from Web UI")
if ssh_process and ssh_process.poll() is None:
try:
if ssh_master is not None:
os.write(ssh_master, b"exit\n")
ssh_process.terminate()
except:
pass
elif cmd == 'start':
add_log_line("[SYSTEM] Start requested from Web UI")
if ssh_process is None or ssh_process.poll() is not None:
start_freeroot()
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 ssh_process:
try:
if ssh_master is not None:
os.write(ssh_master, b"exit\n")
ssh_process.terminate()
ssh_process.wait(timeout=10)
except:
try:
ssh_process.kill()
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)
ssh_thread = threading.Thread(target=start_freeroot, daemon=True)
ssh_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 "✅ FreeRoot 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 "🔄 PTY Support Added - Interactive commands now work!"
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 "⚠️ FreeRoot 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"]