fcfd / app.py
sayed125's picture
Update app.py
37a781e verified
Raw
History Blame Contribute Delete
100 kB
import gradio as gr
import os
import subprocess
import threading
import shutil
import spaces
import asyncio
import json
import time
import re
import base64
try:
import psutil
except ImportError:
# ู‡ุฐุง ุงุญุชูŠุงุทูŠ ูู‚ุท โ€” ูŠุฌุจ ุฃู† ุชูƒูˆู† psutil ู…ุซุจู‘ุชุฉ ุนุจุฑ requirements.txt
subprocess.run(["pip", "install", "psutil"], capture_output=True)
try:
import psutil
except ImportError:
psutil = None # ู„ุง ู†ูˆู‚ู ุงู„ุชุทุจูŠู‚ ุจุงู„ูƒุงู…ู„ ุจุณุจุจ ูุดู„ ุชุซุจูŠุช ู…ูƒุชุจุฉ ู…ุณุงุนุฏุฉ
try:
import huggingface_hub
except ImportError:
# ู‡ุฐุง ุงุญุชูŠุงุทูŠ ูู‚ุท โ€” ูŠุฌุจ ุฃู† ุชูƒูˆู† huggingface_hub ู…ุซุจู‘ุชุฉ ุนุจุฑ requirements.txt
subprocess.run(["pip", "install", "huggingface_hub>=0.20.0"], capture_output=True)
try:
import huggingface_hub
except ImportError:
huggingface_hub = None # ู…ูŠุฒุฉ ุงู„ู…ุฒุงู…ู†ุฉ ุงู„ุณุญุงุจูŠุฉ ุณุชูุนุทูŽู‘ู„ ุจุฏู„ุงู‹ ู…ู† ุชุนุทูŠู„ ูƒู„ ุงู„ุชุทุจูŠู‚
WORKDIR = "/home/user/app/server"
os.makedirs(WORKDIR, exist_ok=True)
log_output = []
mc_process = None
playit_process = None
# โ”€โ”€โ”€ Playit Agent ุงู„ูƒุงู…ู„ (ูŠุฏุนู… ูƒู„ ุฃู†ูˆุงุน ุงู„ุชู†ู„ุงุช: Java TCP + Bedrock UDP + ุฃูŠ UDP ุขุฎุฑ) โ”€โ”€โ”€
# ุจู„ุฌู† playit-minecraft-plugin.jar ุจูŠุนู…ู„ Agent ู…ุตุบู‘ุฑ ุจูŠุฏุนู… ุชู†ู„ ุงู„ุฌุงูุง ูู‚ุท (ุชู„ู‚ุงุฆูŠ).
# ุนุดุงู† ู†ุฏุนู… Bedrock (UDP) ูƒู…ุงู†ุŒ ู„ุงุฒู… Agent ูƒุงู…ู„ ู…ุณุชู‚ู„ โ€” ูˆุฏู‡ ุงู„ู„ูŠ ุจูŠูˆูุฑู‡ ู‡ู†ุง.
PLAYIT_DIR = os.path.join(WORKDIR, "playit")
PLAYIT_BIN = os.path.join(PLAYIT_DIR, "playit-agent")
PLAYIT_SECRET_PATH = os.path.join(PLAYIT_DIR, "playit.toml")
PLAYIT_VERSION = "1.0.10"
PLAYIT_DOWNLOAD_URL = f"https://github.com/playit-cloud/playit-agent/releases/download/v{PLAYIT_VERSION}/playit-linux-amd64"
# โ”€โ”€โ”€ ู†ุธุงู… ุงู„ุตู„ุงุญูŠุงุช ูˆุงู„ู…ุณุชุฎุฏู…ูŠู† โ”€โ”€โ”€
USERS_FILE = os.path.join(WORKDIR, "users.json")
PERMISSIONS = {
"admin": ["console", "start", "stop", "restart", "files_read", "files_write", "files_delete", "users_manage", "cloud_manage"],
"editor": ["console", "files_read", "files_write", "cloud_manage"],
"viewer": ["console", "files_read"],
}
def load_users():
try:
if os.path.exists(USERS_FILE):
with open(USERS_FILE) as f:
return json.load(f)
except: pass
return {}
def save_users(users):
try:
with open(USERS_FILE, "w") as f:
json.dump(users, f, indent=2)
except: pass
def get_user_role(username):
if not username:
return None
users = load_users()
return users.get(username, None)
# โš ๏ธ ู…ู„ุงุญุธุฉ ุฃู…ู†ูŠุฉ ู…ู‡ู…ุฉ:
# ู‡ุฐุง ุงู„ุชุทุจูŠู‚ ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุธุงู… ุชุณุฌูŠู„ ุฏุฎูˆู„ ุญู‚ูŠู‚ูŠ โ€” ู„ุง ูŠูˆุฌุฏ ุฃูŠ ู…ูƒุงู† ููŠ ุงู„ูˆุงุฌู‡ุฉ
# ูŠูุญุฏูŽู‘ุซ ููŠู‡ current_user ุจุงุณู… ู…ุณุชุฎุฏู… ูุนู„ูŠ (ูŠุจู‚ู‰ ุฏุงุฆู…ุงู‹ None). ู„ุฐู„ูƒ ุนู†ุฏู…ุง ุชูƒูˆู†
# LOCAL_DEV_MODE = FalseุŒ ูƒู„ ุงู„ุฃุฒุฑุงุฑ (Start/Stop/Files/...) ุณุชุฑูุถ ุงู„ุชู†ููŠุฐ ุจุตู…ุช
# ู„ุฃู† has_perm(None, ...) ุณุชูุฑุฌุน False ุฏุงุฆู…ุงู‹.
# ู„ู‡ุฐุง ุงู„ุณุจุจ ุฃุนุฏู†ุงู‡ุง ุฅู„ู‰ True ุงูุชุฑุงุถูŠุงู‹ ู„ูŠุนู…ู„ ุงู„ุชุทุจูŠู‚ ูƒู…ุง ุตูู…ู… (ู…ุณุชุฎุฏู… ูˆุงุญุฏ ูŠุฏูŠุฑู‡
# ุตุงุญุจ ุงู„ู€ Space). ุฅุฐุง ูƒุงู† ุงู„ู€ Space ุนุงู…ุงู‹ ูˆูŠุฒูˆุฑู‡ ุฃุดุฎุงุต ุขุฎุฑูˆู†ุŒ ุงู„ุญู„ ุงู„ุตุญูŠุญ ู‡ูˆ
# ุญู…ุงูŠุฉ ุงู„ูˆุตูˆู„ ุนุจุฑ demo.launch(auth=("username","password")) ุฃูˆ ุนุจุฑ ุฌุนู„ ุงู„ู€
# Space ุฎุงุตุงู‹ (Private) ู…ู† ุฅุนุฏุงุฏุงุช ุงู„ุฑูŠุจูˆุŒ ูˆู„ูŠุณ ูู‚ุท ุนุจุฑ ู…ุชุบูŠุฑ LOCAL_DEV_MODE.
LOCAL_DEV_MODE = os.environ.get("LOCAL_DEV_MODE", "true").strip().lower() in ("1", "true", "yes")
def has_perm(username, perm):
if username is None:
return LOCAL_DEV_MODE
role = get_user_role(username)
if role is None:
return False
return perm in PERMISSIONS.get(role, [])
@spaces.GPU
def _zerogpu_probe():
"""
ุฏุงู„ุฉ ุตูˆุฑูŠุฉ ูู‚ุท ู„ุชู„ุจูŠุฉ ุดุฑุท ู…ู†ุตุฉ Hugging Face ุนู„ู‰ ุนุชุงุฏ ZeroGPUุŒ ูˆุงู„ุชูŠ ุชุชุทู„ุจ
ูˆุฌูˆุฏ ุฏุงู„ุฉ ูˆุงุญุฏุฉ ุนู„ู‰ ุงู„ุฃู‚ู„ ู…ุนุฑู‘ูุฉ ุจู€ @spaces.GPU ูˆู‚ุช ุงู„ุฅู‚ู„ุงุนุŒ ูˆุฅู„ุง ูุดู„ ุงู„ู€ Space
ุจุงู„ูƒุงู…ู„ ุจุฎุทุฃ "No @spaces.GPU function detected during startup".
ุนู…ุฏุงู‹ ู„ุง ุชูุณุชุฏุนู‰ ู‡ุฐู‡ ุงู„ุฏุงู„ุฉ ู…ู† ุฃูŠ ู…ูƒุงู† ููŠ ุงู„ุชุทุจูŠู‚ (ู„ุง ุญุงุฌุฉ ูุนู„ูŠุฉ ู„ู€ GPU ู‡ู†ุง)ุŒ
ู„ุชูุงุฏูŠ ุงุณุชู‡ู„ุงูƒ ุญุตุฉ GPU ุงู„ู…ุฌุงู†ูŠุฉ ุฃูˆ ุงู„ุชุณุจุจ ุจุฃุฎุทุงุก ู…ุชูƒุฑุฑุฉ ู…ุน ูƒู„ ุฒูŠุงุฑุฉ.
ุฅุฐุง ุชู… ุชุบูŠูŠุฑ ุนุชุงุฏ ุงู„ู€ Space ุฅู„ู‰ CPUุŒ ูŠู…ูƒู† ุญุฐู ู‡ุฐู‡ ุงู„ุฏุงู„ุฉ ูˆุณุทุฑ `import spaces` ุจุฃู…ุงู†.
"""
return "[SYSTEM] GPU probe (not used)."
def log(message):
log_output.append(message)
if len(log_output) > 400:
log_output.pop(0)
def run_thread(target, *args, name=None, **kwargs):
"""
ูŠุดุบู‘ู„ target ุฏุงุฎู„ ุฎูŠุท ู…ู†ูุตู„ ู…ุน ุงู„ุชู‚ุงุท ุฃูŠ ุงุณุชุซู†ุงุก ุบูŠุฑ ู…ุชูˆู‚ุน ูˆุชุณุฌูŠู„ู‡ ููŠ ุงู„ู„ูˆุฌุŒ
ุจุฏู„ุงู‹ ู…ู† ุฃู† ูŠู…ูˆุช ุงู„ุฎูŠุท ุจุตู…ุช (ูˆู‡ูˆ ู…ุง ูƒุงู† ูŠุณุจุจ ุธู‡ูˆุฑ ุชุนุทู‘ู„ ุบูŠุฑ ู…ูุณูŽู‘ุฑ ููŠ ุงู„ูˆุงุฌู‡ุฉ).
"""
def _wrapper():
try:
target(*args, **kwargs)
except Exception as e:
log(f"[THREAD ERROR] ูุดู„ ุบูŠุฑ ู…ุชูˆู‚ุน ููŠ ุงู„ู…ู‡ู…ุฉ ุงู„ุฎู„ููŠุฉ [{name or target.__name__}]: {e}")
t = threading.Thread(target=_wrapper, daemon=True)
t.start()
return t
# ู‚ูู„ ู„ู…ู†ุน ุชุฒุงู…ู† ุนู…ู„ูŠุชูŠ ุฑูุน (push_to_dataset) ููŠ ู†ูุณ ุงู„ูˆู‚ุช โ€” ูŠู…ู†ุน ุชุนุงุฑุถ/ูุณุงุฏ ุงู„ุฑูุน
_push_lock = threading.Lock()
start_time = time.time()
_last_auto_sync_time = time.time()
# โ”€โ”€โ”€ ู…ุญุฑูƒ ุงู„ุชุฒุงู…ู† ุงู„ู…ุจุงุดุฑ ู…ุน Hugging Face Dataset (Server/) โ”€โ”€โ”€
HF_CONFIG_FILE = os.path.join(WORKDIR, "hf_sync.json")
def load_hf_config():
try:
if os.path.exists(HF_CONFIG_FILE):
with open(HF_CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except: pass
return {
"hf_token": os.environ.get("HF_TOKEN", "").strip(),
"hf_dataset_repo": os.environ.get("HF_DATASET_REPO", "").strip(),
"auto_sync": True,
"sync_interval": 10
}
def save_hf_config(config_dict):
try:
with open(HF_CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config_dict, f, indent=2, ensure_ascii=False)
return True, "โœ… ุชู… ุญูุธ ุฅุนุฏุงุฏุงุช ุงู„ุชุฒุงู…ู† ุงู„ู…ุจุงุดุฑ ู…ุน ุงู„ู€ Dataset ุจู†ุฌุงุญ!"
except Exception as e:
return False, f"โŒ ูุดู„ ุญูุธ ุงู„ุฅุนุฏุงุฏุงุช: {e}"
def get_full_repo_id(repo_id, token):
"""
ุฏุงู„ุฉ ู„ุชุตุญูŠุญ ุงุณู… ุงู„ู…ุณุชูˆุฏุน ุชู„ู‚ุงุฆูŠุงู‹ ุฅุฐุง ุชู… ุฅุฏุฎุงู„ู‡ ุจุฏูˆู† ุงุณู… ุงู„ู…ุณุชุฎุฏู… (ู…ุซู„: SAW).
"""
if not repo_id:
return None
if "/" not in repo_id and token:
try:
from huggingface_hub import HfApi
api = HfApi(token=token)
user_info = api.whoami()
username = user_info.get("name")
if username:
resolved = f"{username}/{repo_id}"
log(f"[DATASET SYNC] ๐Ÿ”„ ุชู… ุชุตุญูŠุญ ุงุณู… ุงู„ู…ุณุชูˆุฏุน ุชู„ู‚ุงุฆูŠุงู‹ ุฅู„ู‰: {resolved}")
return resolved
except Exception as e:
log(f"[DATASET SYNC WARNING] ู„ู… ู†ุชู…ูƒู† ู…ู† ุงู„ุชุนุฑู ุนู„ู‰ ุงุณู… ุงู„ู…ุณุชุฎุฏู… ู„ุชุตุญูŠุญ ุงู„ู…ุณุชูˆุฏุน ุชู„ู‚ุงุฆูŠุงู‹: {e}")
return repo_id
def init_dataset_and_pull():
"""
1. ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุงู„ู€ Dataset ุนู„ู‰ Hugging Face ุฃูˆ ุฅู†ุดุงุคู‡ ููˆุฑุงู‹ ุจุงุณู… ุงู„ู…ุณุชุฎุฏู… ุงู„ุตุญูŠุญ.
2. ุณุญุจ ูˆุชู†ุฒูŠู„ ู…ุญุชูˆูŠุงุช ู…ุฌู„ุฏ Server/ ุฅู„ู‰ WORKDIR.
"""
if huggingface_hub is None:
return False, "โš ๏ธ ู…ูƒุชุจุฉ huggingface_hub ุบูŠุฑ ู…ุชุงุญุฉ โ€” ุณูŠุชู… ุงู„ุนู…ู„ ุนู„ู‰ ุงู„ู…ุฌู„ุฏ ุงู„ู…ุญู„ูŠ ูู‚ุท."
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
msg = "โš ๏ธ ุงู„ุชุฒุงู…ู† ุงู„ู…ุจุงุดุฑ: ู„ู… ูŠุชู… ุถุจุท HF_TOKEN ุฃูˆ HF_DATASET_REPO. ุณูŠุชู… ุงู„ุนู…ู„ ุนู„ู‰ ุงู„ู…ุฌู„ุฏ ุงู„ู…ุญู„ูŠ ูู‚ุท."
log(f"[DATASET SYNC] {msg}")
return False, msg
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "โŒ ุงุณู… ู…ุณุชูˆุฏุน ุงู„ู€ Dataset ุบูŠุฑ ุตุงู„ุญ."
try:
from huggingface_hub import HfApi, snapshot_download
api = HfApi(token=token)
# 1. ุฅู†ุดุงุก ุงู„ู€ Dataset ุฃูˆ ุงู„ุชุญู‚ู‚ ู…ู†ู‡
try:
log(f"[DATASET SYNC] ๐Ÿ›ก๏ธ ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุงู„ู€ Dataset [{resolved_repo_id}] ุฃูˆ ุฅู†ุดุงุคู‡ ุงู„ุขู†...")
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
log(f"[DATASET SYNC] โœ… ุงู„ู€ Dataset [{resolved_repo_id}] ุฌุงู‡ุฒ ูˆู…ุฑุจูˆุท.")
except Exception as create_err:
err_str = str(create_err)
if "403" in err_str or "401" in err_str:
msg = f"โŒ ุฎุทุฃ ุตู„ุงุญูŠุงุช (Forbidden/Unauthorized) ุนู†ุฏ ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ู€ Dataset [{resolved_repo_id}]: ุชุฃูƒุฏ ู…ู† ุตู„ุงุญูŠุฉ Write ู„ู„ุชูˆูƒู† ุงู„ุฎุงุต ุจูƒ."
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# ููŠ ุญุงู„ ูˆุฌูˆุฏ ุฎุทุฃ ุขุฎุฑ ุญุฑุฌ ู†ูˆู‚ู ุงู„ุนู…ู„ูŠุฉ ูˆู„ุง ู†ุญุงูˆู„ ุงู„ุชุญู…ูŠู„ ุฃูˆ ุงู„ุฑูุน
msg = f"โŒ ูุดู„ ุชู‡ูŠุฆุฉ ู…ุณุชูˆุฏุน ุงู„ู€ Dataset [{resolved_repo_id}]: {create_err}"
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# 2. ุชู†ุฒูŠู„ ูˆุณุญุจ ุงู„ู…ู„ูุงุช
local_tmp = os.path.join(WORKDIR, "../_cloud_restore_tmp")
if os.path.exists(local_tmp): shutil.rmtree(local_tmp, ignore_errors=True)
os.makedirs(local_tmp, exist_ok=True)
try:
log(f"[DATASET SYNC] ๐Ÿ“ฅ ุฌุงุฑูŠ ุชู†ุฒูŠู„ ู…ู„ูุงุช ุงู„ุณูŠุฑูุฑ ู…ู† ุงู„ู…ุฌู„ุฏ Server/ ููŠ ุงู„ู€ Dataset [{resolved_repo_id}] ...")
snapshot_download(
repo_id=resolved_repo_id,
repo_type="dataset",
allow_patterns=["Server/*"],
local_dir=local_tmp,
token=token
)
server_cloud_dir = os.path.join(local_tmp, "Server")
if os.path.exists(server_cloud_dir) and os.listdir(server_cloud_dir):
file_count = 0
for root, dirs, files in os.walk(server_cloud_dir):
rel_path = os.path.relpath(root, server_cloud_dir)
dest_dir = os.path.join(WORKDIR, rel_path) if rel_path != "." else WORKDIR
os.makedirs(dest_dir, exist_ok=True)
for f in files:
shutil.copy2(os.path.join(root, f), os.path.join(dest_dir, f))
file_count += 1
shutil.rmtree(local_tmp, ignore_errors=True)
msg = f"โœ… ุชู… ุชู†ุฒูŠู„ ({file_count}) ู…ู„ู ู…ู† ุงู„ู…ุฌู„ุฏ Server/ ุฏุงุฎู„ ุงู„ู€ Dataset [{resolved_repo_id}] ุจู†ุฌุงุญ!"
log(f"[DATASET SYNC] {msg}")
return True, msg
else:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] โ„น๏ธ ุงู„ู…ุฌู„ุฏ Server/ ุบูŠุฑ ู…ูˆุฌูˆุฏ ุจุนุฏ ููŠ ุงู„ู€ Dataset [{resolved_repo_id}]. ุณูŠุชู… ุฅู†ุดุงุคู‡ ูˆุฑูุน ุงู„ู…ู„ูุงุช ุงู„ุขู†...")
push_to_dataset()
return True, f"โœ… ุชู… ุฅู†ุดุงุก ุงู„ู…ุฌู„ุฏ Server/ ุฏุงุฎู„ ุงู„ู€ Dataset [{resolved_repo_id}] ูˆู…ุฒุงู…ู†ุฉ ุงู„ู…ู„ูุงุช."
except Exception as inner_e:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] ุงู„ู…ุฌู„ุฏ Server ุบูŠุฑ ู…ุชูˆูุฑ ุจุนุฏุŒ ุฌุงุฑูŠ ุงู„ุชุฃุณูŠุณ ูˆุงู„ุฑูุน ุงู„ุฃูˆู„ูŠ: {inner_e}")
push_to_dataset()
return True, f"โœ… ุชู… ุชู‡ูŠุฆุฉ ุงู„ู…ุฌู„ุฏ Server/ ููŠ ุงู„ู€ Dataset [{resolved_repo_id}]."
except Exception as e:
err_msg = f"โŒ ุฎุทุฃ ููŠ ุงู„ุงุชุตุงู„ ุจุงู„ู€ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def push_to_dataset():
"""
ู…ุฒุงู…ู†ุฉ ูˆุฑูุน ูƒู„ ุงู„ู…ู„ูุงุช ู…ุจุงุดุฑุฉ ููŠ ุงู„ู…ุฌู„ุฏ Server/ ุฏุงุฎู„ ุงู„ู€ Dataset.
"""
if huggingface_hub is None:
return False, "โš ๏ธ ู…ูƒุชุจุฉ huggingface_hub ุบูŠุฑ ู…ุชุงุญุฉ โ€” ุชุนุฐู‘ุฑ ุชูุนูŠู„ ุงู„ู…ุฒุงู…ู†ุฉ ุงู„ุณุญุงุจูŠุฉ."
if not _push_lock.acquire(blocking=False):
log("[DATASET SYNC] โณ ุนู…ู„ูŠุฉ ุฑูุน ุฃุฎุฑู‰ ู‚ูŠุฏ ุงู„ุชู†ููŠุฐ ุจุงู„ูุนู„ โ€” ุชู… ุชุฌุงู‡ู„ ู‡ุฐุง ุงู„ุทู„ุจ ู„ุชูุงุฏูŠ ุงู„ุชุนุงุฑุถ.")
return False, "โณ ุนู…ู„ูŠุฉ ู…ุฒุงู…ู†ุฉ ุฃุฎุฑู‰ ู‚ูŠุฏ ุงู„ุชู†ููŠุฐ ุจุงู„ูุนู„ุŒ ุญุงูˆู„ ู„ุงุญู‚ุงู‹."
try:
return _push_to_dataset_locked()
finally:
_push_lock.release()
def _push_to_dataset_locked():
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
return False, "โš ๏ธ ุฃุฏุฎู„ HF Token ูˆุงุณู… ุงู„ู€ Dataset ุฃูˆู„ุงู‹ ููŠ ุชุจูˆูŠุจ 'โ˜๏ธ ุงู„ุชุฒุงู…ู† ุงู„ู…ุจุงุดุฑ'."
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "โŒ ุงุณู… ู…ุณุชูˆุฏุน ุงู„ู€ Dataset ุบูŠุฑ ุตุงู„ุญ."
try:
log(f"[DATASET SYNC] ๐Ÿ“ค ุฌุงุฑูŠ ู…ุฒุงู…ู†ุฉ ูˆุฑูุน ู…ู„ูุงุช ุงู„ุณูŠุฑูุฑ ูˆุฅุนุฏุงุฏุงุช ุงู„ู€ IP ุฅู„ู‰ [{resolved_repo_id}]/Server ...")
from huggingface_hub import HfApi
api = HfApi(token=token)
# ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุงู„ู…ุณุชูˆุฏุน ู‚ุจู„ ู…ุญุงูˆู„ุฉ ุงู„ุฑูุน ู„ุชู„ุงููŠ ุฃุฎุทุงุก ุงู„ู€ 404
try:
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
except Exception as ce:
err_str = str(ce)
if "403" in err_str or "401" in err_str or "404" in err_str:
err_msg = f"โŒ ุฎุทุฃ ุฃุฐูˆู†ุงุช ุฃูˆ ุนุฏู… ูˆุฌูˆุฏ ุงู„ู…ุณุชูˆุฏุน ({ce}) ุนู†ุฏ ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ู€ Dataset [{resolved_repo_id}]."
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
# ููŠ ุญุงู„ ูˆุฌูˆุฏ ู…ุดูƒู„ุฉ ุชุฃุณูŠุณูŠุฉ ุฃุฎุฑู‰ ู†ู„ุบูŠ ุงู„ุฑูุน ู„ู…ู†ุน ุชูƒุฑุงุฑ ุงู„ุฃุฎุทุงุก ุงู„ุณุงุจู‚ุฉ
err_msg = f"โŒ ูุดู„ ุชุฃุณูŠุณ ุงู„ู…ุณุชูˆุฏุน [{resolved_repo_id}]: {ce}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
ignore_list = [
"jdk/*", "jdk.tar.gz", "logs/*", "*.lock", "*.tmp", ".cache/*", "_cloud_restore_tmp/*"
]
api.upload_folder(
folder_path=WORKDIR,
path_in_repo="Server",
repo_id=resolved_repo_id,
repo_type="dataset",
token=token,
ignore_patterns=ignore_list,
commit_message=f"Live sync server files & Playit IP -> Server/ ({time.strftime('%Y-%m-%d %H:%M:%S')})"
)
msg = f"โœ… ุชู… ู…ุฒุงู…ู†ุฉ ูˆุฑูุน ุฌู…ูŠุน ู…ู„ูุงุช ุงู„ุณูŠุฑูุฑ ู…ุจุงุดุฑุฉ ุฅู„ู‰ ุงู„ู…ุฌู„ุฏ Server/ ููŠ ุงู„ู€ Dataset [{resolved_repo_id}]!"
log(f"[DATASET SYNC] {msg}")
return True, msg
except Exception as e:
err_msg = f"โŒ ุฎุทุฃ ุฃุซู†ุงุก ุงู„ุฑูุน ุฅู„ู‰ ุงู„ู…ุฌู„ุฏ Server ุฏุงุฎู„ ุงู„ู€ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def dataset_sync_worker():
global _last_auto_sync_time
while True:
time.sleep(30)
try:
config = load_hf_config()
if config.get("auto_sync", True) and (config.get("hf_token") or os.environ.get("HF_TOKEN")) and (config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO")):
interval_mins = int(config.get("sync_interval", 10))
if interval_mins < 1: interval_mins = 10
if time.time() - _last_auto_sync_time >= interval_mins * 60:
_last_auto_sync_time = time.time()
if mc_process and mc_process.poll() is None:
log(f"[DATASET SYNC] ๐Ÿ•’ ุงู„ุชุฒุงู…ู† ุงู„ุฏูˆุฑูŠ ุงู„ุชู„ู‚ุงุฆูŠ ู…ุน ุงู„ู…ุฌู„ุฏ Server ููŠ ุงู„ู€ Dataset (ูƒู„ {interval_mins} ุฏู‚ูŠู‚ุฉ)...")
push_to_dataset()
except Exception as e:
log(f"[DATASET SYNC] Worker error: {e}")
run_thread(dataset_sync_worker, name="dataset_sync_worker")
def get_system_resources():
try:
cpu = psutil.cpu_percent(interval=0.1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
uptime_sec = int(time.time() - start_time)
days = uptime_sec // 86400
hours = (uptime_sec % 86400) // 3600
mins = (uptime_sec % 3600) // 60
uptime_str = f"{days}d {hours}h {mins}m" if days else f"{hours}h {mins}m"
return {
"cpu": round(cpu, 1),
"ram_used": round(mem.used / (1024**3), 1),
"ram_total": round(mem.total / (1024**3), 1),
"ram_pct": round(mem.percent, 1),
"disk_used": round(disk.used / (1024**3), 1),
"disk_total": round(disk.total / (1024**3), 1),
"disk_pct": round(disk.percent, 1),
"uptime": uptime_str,
}
except:
return {"cpu": 0, "ram_used": 0, "ram_total": 4, "ram_pct": 0, "disk_used": 0, "disk_total": 20, "disk_pct": 0, "uptime": "0m"}
def get_online_players():
try:
joined = set()
left = set()
lines = log_output[-300:]
for line in lines:
m = re.search(r"(\S+)\s+joined the game", line)
if m: joined.add(m.group(1))
m = re.search(r"(\S+)\s+left the game", line)
if m: left.add(m.group(1))
return list(joined - left)
except: return []
def send_console_command(cmd, user=None):
if not has_perm(user, "console"):
return "โ›” You don't have `console` permission"
if not cmd or not cmd.strip():
return "โš ๏ธ Enter a command"
if not (mc_process and mc_process.poll() is None):
return "โš ๏ธ Server is not running"
try:
mc_process.stdin.write(cmd.strip() + "\n")
mc_process.stdin.flush()
log(f"[CONSOLE] > {cmd.strip()}")
return f"โœ… Command sent: `{cmd.strip()}`"
except Exception as e:
return f"โŒ Failed to send command: {e}"
def safe_download(url, dest_path, label, timeout=180):
try:
result = subprocess.run(["wget", "-q", "-O", dest_path, url], capture_output=True, text=True, timeout=timeout)
if result.returncode != 0 or not os.path.exists(dest_path) or os.path.getsize(dest_path) == 0:
log(f"[ERROR] ูุดู„ ุชุญู…ูŠู„ {label}: {result.stderr.strip()[:200] if result.stderr else 'ุฑุงุจุท ุบูŠุฑ ุตุงู„ุญ'}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
log(f"[SYSTEM] ุชู… ุชุญู…ูŠู„ {label} ุจู†ุฌุงุญ.")
return True
except subprocess.TimeoutExpired:
log(f"[ERROR] ุงู†ุชู‡ุช ู…ู‡ู„ุฉ ุชุญู…ูŠู„ {label} (timeout).")
if os.path.exists(dest_path): os.remove(dest_path)
return False
except Exception as e:
log(f"[ERROR] ุฎุทุฃ ุฃุซู†ุงุก ุชุญู…ูŠู„ {label}: {e}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
def download_dependencies():
if not os.path.exists(os.path.join(WORKDIR, "jdk")):
jdk_tar = os.path.join(WORKDIR, "jdk.tar.gz")
jdk_url = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jre_x64_linux_hotspot_21.0.4_7.tar.gz"
if safe_download(jdk_url, jdk_tar, "Java 21 (JDK)"):
try:
subprocess.run(["tar", "-xzf", jdk_tar, "-C", WORKDIR], check=True, capture_output=True, text=True)
for extracted in os.listdir(WORKDIR):
if extracted.startswith("jdk-21"):
os.rename(os.path.join(WORKDIR, extracted), os.path.join(WORKDIR, "jdk"))
os.remove(jdk_tar)
except Exception as e:
log(f"[ERROR] ูุดู„ ุงุณุชุฎุฑุงุฌ JDK: {e}")
if not os.path.exists(os.path.join(WORKDIR, "paper.jar")):
safe_download("https://api.purpurmc.org/v2/purpur/1.21.1/latest/download", os.path.join(WORKDIR, "paper.jar"), "Purpur Server (1.21.1)")
plugins_dir = os.path.join(WORKDIR, "plugins")
os.makedirs(plugins_dir, exist_ok=True)
if not os.path.exists(os.path.join(plugins_dir, "playit.jar")):
safe_download("https://github.com/playit-cloud/playit-minecraft-plugin/releases/latest/download/playit-minecraft-plugin.jar", os.path.join(plugins_dir, "playit.jar"), "Playit Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaVersion.jar")):
safe_download("https://cdn.modrinth.com/data/P1OZGk5p/versions/ruzmiBqe/ViaVersion-5.10.0.jar", os.path.join(plugins_dir, "ViaVersion.jar"), "ViaVersion Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaBackwards.jar")):
safe_download("https://cdn.modrinth.com/data/NpvuJQoq/versions/YjpKsm6j/ViaBackwards-5.10.0.jar", os.path.join(plugins_dir, "ViaBackwards.jar"), "ViaBackwards Plugin")
if not os.path.exists(os.path.join(plugins_dir, "Geyser-Spigot.jar")):
safe_download("https://cdn.modrinth.com/data/wKkoqHrH/versions/RHInMdHJ/Geyser-Spigot.jar", os.path.join(plugins_dir, "Geyser-Spigot.jar"), "Geyser-Spigot Plugin")
if not os.path.exists(os.path.join(plugins_dir, "floodgate.jar")):
safe_download("https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot", os.path.join(plugins_dir, "floodgate.jar"), "Floodgate Plugin")
eula_p = os.path.join(WORKDIR, "eula.txt")
if not os.path.exists(eula_p):
with open(eula_p, "w") as f: f.write("eula=true\n")
props_p = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_p):
log("[SYSTEM] Configuring server.properties for cracked mode...")
with open(props_p, "w") as f: f.write("online-mode=false\nspawn-protection=0\n")
def _detect_container_memory_limit_gb():
"""
ูŠุญุงูˆู„ ู‚ุฑุงุกุฉ ุญุฏ ุงู„ุฐุงูƒุฑุฉ ุงู„ูุนู„ูŠ ุงู„ู…ูุฑูˆุถ ุนู„ู‰ ุงู„ุญุงูˆูŠุฉ ู†ูุณู‡ุง (cgroup)ุŒ ู„ุฃู† psutil
ุฃุญูŠุงู†ุงู‹ (ุฎุตูˆุตุงู‹ ุฏุงุฎู„ ุญุงูˆูŠุงุช ู…ุซู„ HF Spaces) ูŠูุฑุฌุน ุฐุงูƒุฑุฉ ุงู„ุฌู‡ุงุฒ ุงู„ู…ุถูŠู ุงู„ูƒุงู…ู„ุฉ
ุจุฏู„ ุญุฏ ุงู„ุญุงูˆูŠุฉ ุงู„ูุนู„ูŠ โ€” ูˆู‡ูˆ ู…ุง ุชุณุจุจ ุณุงุจู‚ุงู‹ ููŠ ู‚ูŠู…ุฉ ุฎูŠุงู„ูŠุฉ ู…ุซู„ -Xmx999G ูˆูƒุงุฏ
ูŠุชุณุจุจ ููŠ ู…ุดุงูƒู„ ุฎุทูŠุฑุฉ.
"""
for path in ("/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"):
try:
with open(path) as f:
raw = f.read().strip()
if raw and raw != "max":
val_gb = int(raw) / (1024**3)
if 0 < val_gb < 1024: # ู‚ูŠู…ุฉ ู…ุนู‚ูˆู„ุฉ (ุฃู‚ู„ ู…ู† ุชูŠุฑุงุจุงูŠุช) ูˆุฅู„ุง ู†ุชุฌุงู‡ู„ู‡ุง
return val_gb
except Exception:
continue
return None
def get_safe_jvm_heap():
"""
ูŠุญุฏุฏ ู‚ูŠู… -Xms/-Xmx ุจุฃู…ุงู† ุจู†ุงุกู‹ ุนู„ู‰ ุงู„ุฐุงูƒุฑุฉ ุงู„ูุนู„ูŠุฉ ุงู„ู…ุชุงุญุฉ ุนู„ู‰ ุนุชุงุฏ ุงู„ู€ SpaceุŒ
ุจุฏู„ุงู‹ ู…ู† ู‚ูŠู…ุฉ ุซุงุจุชุฉ (4GB) ู‚ุฏ ุชุชุฌุงูˆุฒ ุญุฏ ุงู„ุญุงูˆูŠุฉ ูˆุชุณุจุจ OOM Kill ู…ูุงุฌุฆ ู„ู„ู€ Space.
ูŠู…ูƒู† ุงู„ุชุญูƒู… ุจู‡ุง ูŠุฏูˆูŠุงู‹ ุนุจุฑ ู…ุชุบูŠุฑุงุช ุงู„ุจูŠุฆุฉ MC_XMS / MC_XMX ุนู†ุฏ ุงู„ุญุงุฌุฉ.
"""
env_xms, env_xmx = os.environ.get("MC_XMS"), os.environ.get("MC_XMX")
if env_xms and env_xmx:
return env_xms, env_xmx
cgroup_gb = _detect_container_memory_limit_gb()
try:
psutil_gb = psutil.virtual_memory().total / (1024**3) if psutil else 4
except Exception:
psutil_gb = 4
# ู†ูุถู‘ู„ ุญุฏ ุงู„ู€ cgroup (ุญุฏ ุงู„ุญุงูˆูŠุฉ ุงู„ูุนู„ูŠ) ู„ูˆ ู…ุชุงุญ ูˆู…ู†ุทู‚ูŠุŒ ุจุฏู„ ุฑู‚ู… ุงู„ุฌู‡ุงุฒ ุงู„ู…ุถูŠู ุงู„ูƒุงู…ู„
total_gb = cgroup_gb if cgroup_gb else psutil_gb
# ู†ุชุฑูƒ ู‡ุงู…ุดุงู‹ ูƒุงููŠุงู‹ ู„ู†ุธุงู… ุงู„ุชุดุบูŠู„ ูˆGradio ูˆุนู…ู„ูŠุงุช ุงู„ู…ุฒุงู…ู†ุฉุŒ ูˆู„ุง ู†ุณุชู‡ู„ูƒ ูƒู„ ุงู„ุฐุงูƒุฑุฉ ุนู„ู‰ ุงู„ู€ heap
safe_max_gb = max(1, int(total_gb * 0.5))
# โš ๏ธ ุณู‚ู ุฃู…ุงู† ุตุงุฑู… ุบูŠุฑ ู‚ุงุจู„ ู„ู„ุชุฌุงูˆุฒ ู…ู‡ู…ุง ูƒุงู†ุช ู†ุชูŠุฌุฉ ุงู„ุงูƒุชุดุงู ุฃุนู„ุงู‡ โ€” ู‡ุฐุง ูŠู…ู†ุน ุชูƒุฑุงุฑ
# ู…ุดูƒู„ุฉ -Xmx999G ููŠ ุญุงู„ ุฃุนุทุช ุฃูŠ ุทุฑูŠู‚ุฉ ุงูƒุชุดุงู ู‚ูŠู…ุฉ ุบูŠุฑ ู…ูˆุซูˆู‚ุฉ ุนู„ู‰ ุฃูŠ ู…ู†ุตุฉ ู…ุณุชู‚ุจู„ุงู‹.
HARD_CAP_GB = 8
safe_max_gb = min(safe_max_gb, HARD_CAP_GB)
xmx = f"{safe_max_gb}G"
xms = f"{max(1, safe_max_gb // 2)}G"
return xms, xmx
def download_playit_agent():
"""
ูŠู†ุฒู‘ู„ ุงู„ูˆูƒูŠู„ ุงู„ูƒุงู…ู„ (playit-agent) ู…ุฑุฉ ูˆุงุญุฏุฉ ูู‚ุท ูˆูŠุนุทูŠู‡ ุตู„ุงุญูŠุฉ ุชู†ููŠุฐ.
"""
os.makedirs(PLAYIT_DIR, exist_ok=True)
if os.path.isfile(PLAYIT_BIN) and os.access(PLAYIT_BIN, os.X_OK):
return True
ok = safe_download(PLAYIT_DOWNLOAD_URL, PLAYIT_BIN, f"Playit Full Agent v{PLAYIT_VERSION}")
if ok:
try:
os.chmod(PLAYIT_BIN, 0o755)
except Exception as e:
log(f"[PLAYIT ERROR] ูุดู„ ุถุจุท ุตู„ุงุญูŠุงุช ุชุดุบูŠู„ playit agent: {e}")
return False
return ok
def start_playit_agent():
"""
ูŠุดุบู‘ู„ playit agent ุงู„ูƒุงู…ู„ (ู…ู†ูุตู„ ุชู…ุงู…ุงู‹ ุนู† ุจู„ุฌู† playit-minecraft-plugin.jar).
ุฃูˆู„ ู…ุฑุฉ: ู‡ูŠุทุจุน ุฑุงุจุท claim ููŠ ุงู„ู„ูˆุฌ โ€” ุงูุชุญู‡ ููŠ ุงู„ู…ุชุตูุญ ูˆุณุฌู‘ู„ ุฏุฎูˆู„ ู„ุฑุจุท ุงู„ู€ Agent ุจุญุณุงุจูƒ.
ุจุนุฏู‡ุง ู…ู† https://playit.gg/tunnels ุงุนู…ู„ ุชู†ู„ูŠู† ุนู„ู‰ ู†ูุณ ุงู„ู€ Agent ุงู„ุฌุฏูŠุฏ ุฏู‡:
1) Minecraft Java (TCP) -> local port 25565
2) Minecraft Bedrock/UDP -> local port 19132
ุงู„ู…ุฑุงุช ุงู„ุฌุงูŠุฉ: ุงู„ุณุฑ (secret) ู‡ูŠุชุญูุธ ุชู„ู‚ุงุฆูŠุงู‹ ููŠ playit/playit.toml (ุจูŠุชุฒุงู…ู† ู…ุน ุงู„ู€
Dataset ุฒูŠ ุจุงู‚ูŠ ู…ู„ูุงุช ุงู„ุณูŠุฑูุฑ)ุŒ ูู…ุด ู‡ูŠุญุชุงุฌ claim ุชุงู†ูŠ ุจุนุฏ ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุงู„ู€ Space.
"""
global playit_process
if playit_process and playit_process.poll() is None:
log("[PLAYIT] ุงู„ูˆูƒูŠู„ ุงู„ูƒุงู…ู„ ุดุบุงู„ ุจุงู„ูุนู„.")
return
if not download_playit_agent():
log("[PLAYIT ERROR] โŒ ูุดู„ ุชุญู…ูŠู„ playit agent ุงู„ูƒุงู…ู„ โ€” ุชู†ู„ Bedrock ู„ู† ูŠุนู…ู„ ุชู„ู‚ุงุฆูŠุงู‹. ุฃุนุฏ ุงู„ู…ุญุงูˆู„ุฉ ุจุงู„ุถุบุท ุนู„ู‰ Start.")
return
try:
log("[PLAYIT] ๐Ÿš€ ุฌุงุฑูŠ ุชุดุบูŠู„ playit agent ุงู„ูƒุงู…ู„ (ูŠุฏุนู… Java + Bedrock + ุฃูŠ UDP)...")
playit_process = subprocess.Popen(
[PLAYIT_BIN, "--secret-path", PLAYIT_SECRET_PATH],
cwd=PLAYIT_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in iter(playit_process.stdout.readline, ''):
if not line:
continue
clean = line.strip()
log(f"[PLAYIT] {clean}")
if "playit.gg/claim" in clean:
log("[PLAYIT] โš ๏ธ ุงูุชุญ ุงู„ุฑุงุจุท ุงู„ู„ูŠ ููˆู‚ ููŠ ุงู„ู…ุชุตูุญ ูˆุณุฌู‘ู„ ุฏุฎูˆู„ ู„ุชูุนูŠู„ ุงู„ู€ Agent. ุจุนุฏูŠู† ู…ู† https://playit.gg/tunnels ุงุนู…ู„ ุชู†ู„ Java (TCP 25565) ูˆุชู†ู„ Bedrock (UDP 19132) ุนู„ู‰ ู†ูุณ ุงู„ู€ Agent ุฏู‡.")
playit_process.stdout.close()
exit_code = playit_process.poll()
log(f"[PLAYIT] ุชูˆู‚ู ูˆูƒูŠู„ playit ุงู„ูƒุงู…ู„ (exit code: {exit_code}).")
except Exception as e:
log(f"[PLAYIT ERROR] ูุดู„ ุชุดุบูŠู„ playit agent: {e}")
finally:
playit_process = None
def start_server_backend():
global mc_process
if mc_process and mc_process.poll() is None:
log("[SYSTEM] Server is already running!")
return
try:
# 1. ุชู†ุฒูŠู„ ูˆุณุญุจ ู…ุญุชูˆูŠุงุช ุงู„ู…ุฌู„ุฏ Server/ ู…ู† ุงู„ู€ Dataset
init_dataset_and_pull()
# 2. ุชู†ุฒูŠู„ ุงู„ุชุจุนูŠุงุช ุงู„ู…ุทู„ูˆุจุฉ ููŠ ุญุงู„ ุนุฏู… ูˆุฌูˆุฏู‡ุง
download_dependencies()
# 3. ู…ุฒุงู…ู†ุฉ ููˆุฑูŠุฉ ู„ุชุฃูƒูŠุฏ ูˆุฌูˆุฏ ุงู„ู…ุฌู„ุฏ Server/ ูˆุงู„ู…ู„ูุงุช ุฏุงุฎู„ ุงู„ู€ Dataset ู‚ุจู„ ุจุฏุก ู…ุงูŠู† ูƒุฑุงูุช
push_to_dataset()
# 4. ุชุดุบูŠู„ ุงู„ูˆูƒูŠู„ ุงู„ูƒุงู…ู„ ู„ู€ playit ุจุฎูŠุท ู…ู†ูุตู„ (ูŠุฏุนู… Bedrock/UDP ุจุฌุงู†ุจ ุชู†ู„ ุงู„ุฌุงูุง ุงู„ุชู„ู‚ุงุฆูŠ)
run_thread(start_playit_agent, name="start_playit_agent")
java_path = os.path.join(WORKDIR, "jdk/bin/java")
paper_path = os.path.join(WORKDIR, "paper.jar")
# ูุญุต ูˆุงุถุญ ู‚ุจู„ ุงู„ุชุดุบูŠู„ ุจุฏู„ ุงู„ุณู…ุงุญ ู„ู€ FileNotFoundError ุจุฅุณู‚ุงุท ุงู„ุฎูŠุท ุจุตู…ุช
if not os.path.isfile(java_path):
log("[ERROR] โŒ ุชุนุฐู‘ุฑ ุฅูŠุฌุงุฏ Java (jdk/bin/java) โ€” ูุดู„ ุชู†ุฒูŠู„ JDK ุนู„ู‰ ุงู„ุฃุฑุฌุญ. ุชุญู‚ู‚ ู…ู† ุงุชุตุงู„ ุงู„ุฅู†ุชุฑู†ุช ูˆุฃุนุฏ ุงู„ู…ุญุงูˆู„ุฉ.")
return
if not os.path.isfile(paper_path):
log("[ERROR] โŒ ุชุนุฐู‘ุฑ ุฅูŠุฌุงุฏ paper.jar โ€” ูุดู„ ุชู†ุฒูŠู„ ุณูŠุฑูุฑ Purpur. ุชุญู‚ู‚ ู…ู† ุฑุงุจุท ุงู„ุชู†ุฒูŠู„ ูˆุฃุนุฏ ุงู„ู…ุญุงูˆู„ุฉ.")
return
xms, xmx = get_safe_jvm_heap()
log(f"[SYSTEM] Starting Minecraft Server (Purpur 1.21.1) with -Xms{xms} -Xmx{xmx} ...")
mc_process = subprocess.Popen(
[java_path, f"-Xms{xms}", f"-Xmx{xmx}", "-jar", "paper.jar", "nogui"],
cwd=WORKDIR, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in iter(mc_process.stdout.readline, ''):
if line: log(f"[MINECRAFT] {line.strip()}")
mc_process.stdout.close()
exit_code = mc_process.poll()
log(f"[SYSTEM] โš ๏ธ ุชูˆู‚ูุช ุนู…ู„ูŠุฉ Minecraft (exit code: {exit_code}). ุณูŠุชู… ุชุญุฏูŠุซ ุงู„ุญุงู„ุฉ ุฅู„ู‰ 'ู…ุชูˆู‚ู'.")
except Exception as e:
log(f"[ERROR] โŒ ูุดู„ ุบูŠุฑ ู…ุชูˆู‚ุน ุฃุซู†ุงุก ุชุดุบูŠู„ ุงู„ุณูŠุฑูุฑ: {e}")
finally:
# ู…ู‡ู…: ุจุฏูˆู† ู‡ุฐุง ูƒุงู†ุช ุงู„ูˆุงุฌู‡ุฉ ุชุณุชู…ุฑ ุจุนุฑุถ "Running" ุฑุบู… ุชูˆู‚ู ุงู„ุนู…ู„ูŠุฉ ูุนู„ูŠุงู‹
mc_process = None
run_thread(push_to_dataset, name="push_to_dataset_after_stop")
def control_server(action, user):
perm = action.lower()
if not has_perm(user, perm):
return f"โ›” You don't have `{perm}` permission"
global mc_process
if action == "START":
if mc_process and mc_process.poll() is None:
return "โŒ ุงู„ุณูŠุฑูุฑ ูŠุนู…ู„ ุจุงู„ูุนู„ ุงู„ุขู†!"
run_thread(start_server_backend, name="start_server_backend")
return "โšก ุฌุงุฑูŠ ุชุดุบูŠู„ ุฎุงุฏู… ู…ุงูŠู† ูƒุฑุงูุช ุงู„ุขู† (ู…ุน ุชู†ุฒูŠู„ ูˆุฑูุน ุงู„ู…ู„ูุงุช ู…ุจุงุดุฑุฉ ู…ู† ุงู„ู€ Dataset)..."
elif action == "STOP":
if mc_process and mc_process.poll() is None:
try:
mc_process.stdin.write("/stop\n")
mc_process.stdin.flush()
log("[SYSTEM] Sent /stop command โ€” waiting for graceful shutdown...")
except: pass
deadline = time.time() + 8
while time.time() < deadline:
if mc_process.poll() is not None: break
time.sleep(0.5)
if mc_process and mc_process.poll() is None:
mc_process.terminate()
time.sleep(1)
mc_process = None
log("[SYSTEM] Server stopped by user.")
# ู…ุฒุงู…ู†ุฉ ู…ุญุชูˆูŠุงุช ุงู„ุณูŠุฑูุฑ ููˆุฑุงู‹ ุฅู„ู‰ ู…ุฌู„ุฏ Server ุฏุงุฎู„ ุงู„ู€ Dataset ุนู†ุฏ ุงู„ุฅูŠู‚ุงู
log("[SYSTEM] Syncing directly to Server/ inside Dataset upon stop...")
run_thread(push_to_dataset, name="push_to_dataset")
return "๐Ÿ›‘ Server stopped gracefully โ€” Synced directly to Dataset/Server."
return "โš ๏ธ Server is already stopped."
elif action == "RESTART":
if mc_process and mc_process.poll() is None:
mc_process.terminate()
mc_process = None
log("[SYSTEM] Restarting server...")
run_thread(start_server_backend, name="start_server_backend")
return "๐Ÿ”„ ุฌุงุฑูŠ ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุงู„ุณูŠุฑูุฑ..."
async def get_logs():
return "\n".join(log_output)
async def get_server_status():
status = "๐ŸŸข **Running**" if (mc_process and mc_process.poll() is None) else "๐Ÿ”ด **Stopped**"
return f"## {status}\n\n**Purpur 1.21.1** โ€” Java 21 โ€” 2-4GB RAM"
BINARY_EXTENSIONS = {
".jar", ".zip", ".gz", ".tar", ".rar", ".7z", ".class",
".dat", ".mca", ".mcr", ".png", ".jpg", ".jpeg", ".gif",
".ico", ".exe", ".bin", ".db", ".so", ".dll"
}
MAX_EDITABLE_SIZE = 200 * 1024
PAGE_SIZE = 200
def get_safe_path(rel_path):
if rel_path is None: rel_path = ""
abs_path = os.path.abspath(os.path.join(WORKDIR, rel_path))
if abs_path.startswith(os.path.abspath(WORKDIR)): return abs_path
return WORKDIR
async def scan_directory(current_rel):
target_dir = get_safe_path(current_rel)
folders = []
text_files = []
binary_count = 0
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target_dir))), timeout=5.0)
for item in entries:
full_path = os.path.join(target_dir, item)
try:
if await asyncio.to_thread(os.path.isdir, full_path): folders.append(item)
else:
ext = os.path.splitext(item)[1].lower()
if ext in BINARY_EXTENSIONS: binary_count += 1
else: text_files.append(item)
except: text_files.append(item)
except Exception as e:
log(f"[FILE MGR ERROR] {e}")
folders.sort()
text_files.sort()
folder_choices = [".. (ุงู„ู…ุฌู„ุฏ ุงู„ุฃุนู„ู‰)"] + folders if current_rel else folders
file_choices = text_files if text_files else ["ู„ุง ุชูˆุฌุฏ ู…ู„ูุงุช ู†ุตูŠุฉ ููŠ ู‡ุฐุง ุงู„ู…ุณุงุฑ"]
path_display = f"๐Ÿ“ ุงู„ู…ุณุงุฑ ุงู„ุญุงู„ูŠ ุงู„ู…ูุชูˆุญ: /root/{current_rel}" if current_rel else "๐Ÿ“ ุงู„ู…ุณุงุฑ ุงู„ุญุงู„ูŠ ุงู„ู…ูุชูˆุญ: /root (ุงู„ู…ุฌู„ุฏ ุงู„ุฑุฆูŠุณูŠ)"
if binary_count > 0: path_display += f" \n๐Ÿ“Ž {binary_count} ู…ู„ู ุซู†ุงุฆูŠ ู…ุฎููŠ (jar/zip/png...)"
return path_display, gr.Dropdown(choices=folder_choices, value=None), gr.Dropdown(choices=file_choices, value=None)
async def enter_folder(selected_folder, current_rel):
if not selected_folder: return current_rel, *await scan_directory(current_rel)
if selected_folder == ".. (ุงู„ู…ุฌู„ุฏ ุงู„ุฃุนู„ู‰)":
if not current_rel: return "", *await scan_directory("")
parent_rel = "/".join(current_rel.strip("/").split("/")[:-1])
return parent_rel, *await scan_directory(parent_rel)
else:
new_rel = os.path.join(current_rel, selected_folder).strip("/")
return new_rel, *await scan_directory(new_rel)
def is_probably_binary(file_path, chunk_size=1024):
try:
with open(file_path, "rb") as f: return b"\x00" in f.read(chunk_size)
except: return True
async def load_file_content(selected_file, current_rel):
empty, title_reset = gr.update(value=""), gr.update(value="### ๐Ÿ“ ุชุนุฏูŠู„ ุงู„ู…ู„ู: โ€”")
if not selected_file or "ู„ุง ุชูˆุฌุฏ ู…ู„ูุงุช" in selected_file:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value="๐Ÿ‘ˆ ุงุฎุชุฑ ู…ู„ูุงู‹ ู†ุตูŠุงู‹ ู…ู† ุงู„ู‚ุงุฆู…ุฉ ู„ูŠุธู‡ุฑ ุตู†ุฏูˆู‚ ุงู„ุชุนุฏูŠู„ ู‡ู†ุง."), "", 0, gr.update(visible=False)
file_path = os.path.join(get_safe_path(current_rel), selected_file)
if not os.path.isfile(file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"โš ๏ธ [{selected_file}] ู…ุฌู„ุฏ ูˆู„ูŠุณ ู…ู„ูุงู‹."), "", 0, gr.update(visible=False)
ext = os.path.splitext(selected_file)[1].lower()
if ext in BINARY_EXTENSIONS:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"๐Ÿšซ ุงู„ู…ู„ู [{selected_file}] ุซู†ุงุฆูŠ (Binary) ูˆู„ุง ูŠู…ูƒู† ุชุญุฑูŠุฑู‡ ู†ุตูŠุงู‹."), "", 0, gr.update(visible=False)
try: size = await asyncio.to_thread(os.path.getsize, file_path)
except Exception as e: return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"ุฎุทุฃ ุฃุซู†ุงุก ู‚ุฑุงุกุฉ ุงู„ุญุฌู…: {e}"), "", 0, gr.update(visible=False)
if size > MAX_EDITABLE_SIZE:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"๐Ÿšซ ุงู„ู…ู„ู [{selected_file}] ุญุฌู…ู‡ ูƒุจูŠุฑ ุฌุฏุงู‹ ({size/1024/1024:.2f} MB)."), "", 0, gr.update(visible=False)
if await asyncio.to_thread(is_probably_binary, file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"๐Ÿšซ ุงู„ู…ู„ู [{selected_file}] ูŠุญุชูˆูŠ ุนู„ู‰ ุจูŠุงู†ุงุช ุซู†ุงุฆูŠุฉ."), "", 0, gr.update(visible=False)
try:
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total_lines, display_lines = len(lines), lines[:PAGE_SIZE]
has_more = total_lines > PAGE_SIZE
info = f"### ๐Ÿ“ ุชุนุฏูŠู„ ุงู„ู…ู„ู: `{selected_file}`" + (f" โ€” (ุฃูˆู„ {PAGE_SIZE} ู…ู† {total_lines} ุณุทุฑ)" if has_more else "")
return gr.update(value="".join(display_lines)), gr.update(value=info), gr.update(visible=True), gr.update(visible=False), file_path, PAGE_SIZE, gr.update(visible=has_more)
except Exception as e:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"ุฎุทุฃ ุฃุซู†ุงุก ุงู„ู‚ุฑุงุกุฉ: {e}"), "", 0, gr.update(visible=False)
async def load_more_content(file_path, load_offset):
if not file_path or not os.path.isfile(file_path): return gr.update(), 0, gr.update(visible=False)
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total, new_offset = len(lines), min(load_offset + PAGE_SIZE, len(lines))
return gr.update(value="".join(lines[:new_offset])), new_offset, gr.update(visible=new_offset < total)
async def save_file_content(selected_file, content, current_rel, user):
if not has_perm(user, "files_write"): return "โ›” You don't have `files_write` permission"
if not selected_file or "ู„ุง ุชูˆุฌุฏ ู…ู„ูุงุช" in selected_file: return "โŒ ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ู…ู„ู ุตุงู„ุญ ู„ุญูุธู‡."
file_path = os.path.join(get_safe_path(current_rel), selected_file)
try:
await asyncio.to_thread(lambda: open(file_path, "w", encoding="utf-8").write(content))
run_thread(push_to_dataset, name="push_to_dataset")
return f"โœ… ุชู… ุญูุธ ุงู„ู…ู„ู [{selected_file}] ูˆู…ุฒุงู…ู†ุชูู‡ ู…ุน ุงู„ู€ Dataset!"
except Exception as e: return f"โŒ ูุดู„ ุงู„ุญูุธ: {e}"
async def create_item(name, item_type, current_rel, user):
if not has_perm(user, "files_write"): return "โ›” You don't have `files_write` permission", *await scan_directory(current_rel)
if not name: return "โš ๏ธ ุงู„ุฑุฌุงุก ุฅุฏุฎุงู„ ุงุณู… ุงู„ู…ุฌู„ุฏ ุฃูˆ ุงู„ู…ู„ู ุฃูˆู„ุงู‹.", *await scan_directory(current_rel)
target_path = os.path.join(get_safe_path(current_rel), name)
try:
if "Folder" in item_type or "ู…ุฌู„ุฏ" in item_type:
await asyncio.to_thread(os.makedirs, target_path, exist_ok=True)
msg = f"๐Ÿ“ Created folder [{name}]"
else:
await asyncio.to_thread(lambda: open(target_path, "w", encoding="utf-8").write(""))
msg = f"๐Ÿ“„ Created file [{name}]"
run_thread(push_to_dataset, name="push_to_dataset")
return msg, *await scan_directory(current_rel)
except Exception as e: return f"โŒ ูุดู„ ุงู„ุฅู†ุดุงุก: {e}", *await scan_directory(current_rel)
async def rename_item(old_name, new_name, current_rel, user):
if not has_perm(user, "files_write"): return "โ›” You don't have `files_write` permission", *await scan_directory(current_rel)
if not old_name or not new_name: return "โš ๏ธ ุญุฏุฏ ุงู„ุนู†ุตุฑ ูˆุงูƒุชุจ ุงู„ุงุณู… ุงู„ุฌุฏูŠุฏ.", *await scan_directory(current_rel)
try:
await asyncio.to_thread(os.rename, os.path.join(get_safe_path(current_rel), old_name), os.path.join(get_safe_path(current_rel), new_name))
run_thread(push_to_dataset, name="push_to_dataset")
return f"โœ๏ธ ุชู… ุฅุนุงุฏุฉ ุงู„ุชุณู…ูŠุฉ ุจู†ุฌุงุญ ุฅู„ู‰ [{new_name}].", *await scan_directory(current_rel)
except Exception as e: return f"โŒ ุฎุทุฃ ุฃุซู†ุงุก ุงู„ุชุณู…ูŠุฉ: {e}", *await scan_directory(current_rel)
async def delete_item(item_name, is_folder, current_rel, user):
if not has_perm(user, "files_delete"): return "โ›” You don't have `files_delete` permission", *await scan_directory(current_rel)
if not item_name or "ู„ุง ุชูˆุฌุฏ ู…ู„ูุงุช" in item_name: return "โš ๏ธ ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ุนู†ุตุฑ ู„ุญุฐูู‡.", *await scan_directory(current_rel)
target_p = os.path.join(get_safe_path(current_rel), item_name)
try:
if is_folder:
await asyncio.to_thread(shutil.rmtree, target_p)
msg = f"๐Ÿ—‘๏ธ ุชู… ุญุฐู ุงู„ู…ุฌู„ุฏ ูˆู…ุญุชูˆูŠุงุชู‡ [{item_name}] ุจุงู„ูƒุงู…ู„."
else:
await asyncio.to_thread(os.remove, target_p)
msg = f"๐Ÿ—‘๏ธ ุชู… ุญุฐู ุงู„ู…ู„ู [{item_name}]."
run_thread(push_to_dataset, name="push_to_dataset")
return msg, *await scan_directory(current_rel)
except Exception as e: return f"โŒ ูุดู„ ุงู„ุญุฐู: {e}", *await scan_directory(current_rel)
async def upload_to_current_dir(file_objs, current_rel, user):
if not has_perm(user, "files_write"): return "โ›” You don't have `files_write` permission", *await scan_directory(current_rel)
if not file_objs: return "โŒ ู„ู… ูŠุชู… ุงุฎุชูŠุงุฑ ู…ู„ูุงุช ู„ู„ุฑูุน.", *await scan_directory(current_rel)
target_dir, uploaded_list = get_safe_path(current_rel), []
if not isinstance(file_objs, list): file_objs = [file_objs]
for f_obj in file_objs:
orig_name = getattr(f_obj, 'orig_name', os.path.basename(f_obj.name))
await asyncio.to_thread(shutil.copy, f_obj.name, os.path.join(target_dir, orig_name))
uploaded_list.append(orig_name)
run_thread(push_to_dataset, name="push_to_dataset")
return f"โœ… ุชู… ุฑูุน {len(uploaded_list)} ู…ู„ูุงุช ูˆู…ุฒุงู…ู†ุชูู‡ุง ุจู†ุฌุงุญ!", *await scan_directory(current_rel)
# ุงู„ุณูŠุฑูุฑ ุณูŠุนู…ู„ ูู‚ุท ุนู†ุฏ ุถุบุทูƒ ูŠุฏูˆูŠุงู‹ ุนู„ู‰ ุฒุฑ "โ–ถ Start Server" ุจุนุฏ ุชุฌู‡ูŠุฒ ุงู„ู€ Dataset
def on_space_boot():
log("[SYSTEM] Space booted. Initializing Hugging Face Dataset connection (`Server/` directory)...")
init_dataset_and_pull()
log("[SYSTEM] Ready! Click 'โ–ถ Start Server' from the Console tab to boot Minecraft manually.")
run_thread(on_space_boot, name="on_space_boot")
# โ”€โ”€โ”€ ูˆุงุฌู‡ุฉ PTERODACTYL UI DASHBOARD โ”€โ”€โ”€
DASHBOARD_HTML = r"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg-primary:#0b0e14;
--bg-secondary:#111622;
--bg-card:#161c2c;
--bg-card-hover:#1c2438;
--bg-input:#0e131d;
--bg-terminal:#070a0f;
--border:rgba(255,255,255,0.08);
--border-focus:rgba(34,197,94,0.5);
--text-primary:#f1f5f9;
--text-secondary:#94a3b8;
--text-muted:#475569;
--mc-green:#22c55e;
--mc-green-glow:rgba(34,197,94,0.25);
--mc-red:#ef4444;
--mc-red-glow:rgba(239,68,68,0.25);
--mc-gold:#eab308;
--mc-blue:#0ea5e9;
--radius-sm:8px;
--radius-md:12px;
--radius-lg:16px;
}
body{font-family:'Inter',sans-serif;background:var(--bg-primary);color:var(--text-primary);overflow:hidden;height:100vh}
::selection{background:var(--mc-green);color:#000}
.topbar{
height:64px;background:var(--bg-secondary);border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;padding:0 28px;
box-shadow:0 4px 20px rgba(0,0,0,0.4);z-index:100;position:relative;
}
.topbar-left{display:flex;align-items:center;gap:14px}
.topbar-logo{font-size:20px;font-weight:800;letter-spacing:-0.5px;background:linear-gradient(135deg,#22c55e,#0ea5e9);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.topbar-badge{font-size:11px;font-weight:600;padding:4px 10px;background:rgba(255,255,255,0.06);border:1px solid var(--border);border-radius:20px;color:var(--text-secondary)}
.topbar-right{display:flex;align-items:center;gap:16px}
.status-pill{
display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:30px;
font-size:13px;font-weight:600;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);
}
.status-pill.running{background:rgba(34,197,94,0.12);border:1px solid rgba(34,197,94,0.3);color:var(--mc-green);box-shadow:0 0 15px var(--mc-green-glow)}
.status-pill.stopped{background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.3);color:var(--mc-red);box-shadow:0 0 15px var(--mc-red-glow)}
.status-dot{width:8px;height:8px;border-radius:50%}
.status-dot.running{background:var(--mc-green);box-shadow:0 0 8px var(--mc-green)}
.status-dot.stopped{background:var(--mc-red)}
.layout{display:flex;height:calc(100vh - 64px)}
.sidebar{
width:250px;min-width:250px;background:var(--bg-secondary);border-right:1px solid var(--border);
padding:20px 12px;display:flex;flex-direction:column;gap:6px;overflow-y:auto;
}
.sidebar-title{font-size:11px;font-weight:700;color:var(--text-muted);letter-spacing:1.5px;text-transform:uppercase;padding:8px 14px 12px}
.nav-btn{
display:flex;align-items:center;gap:12px;padding:12px 16px;border-radius:var(--radius-md);
font-size:14px;font-weight:500;color:var(--text-secondary);background:transparent;
border:none;cursor:pointer;transition:all 0.2s;text-align:left;position:relative;
}
.nav-btn:hover{background:var(--bg-card);color:var(--text-primary)}
.nav-btn.active{
background:rgba(34,197,94,0.1);color:var(--mc-green);font-weight:600;
border:1px solid rgba(34,197,94,0.2);
}
.nav-btn.active::before{
content:'';position:absolute;left:0;top:20%;bottom:20%;width:4px;
background:var(--mc-green);border-radius:0 4px 4px 0;
}
.main-content{flex:1;padding:28px 36px;overflow-y:auto;background:var(--bg-primary)}
.tab-pane{display:none;animation:fadeIn 0.25s ease}
.tab-pane.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.page-header{margin-bottom:24px}
.page-title{font-size:24px;font-weight:700;letter-spacing:-0.5px;margin-bottom:6px}
.page-desc{font-size:14px;color:var(--text-secondary)}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:24px}
.stat-card{
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);
padding:20px;position:relative;overflow:hidden;transition:all 0.3s;
}
.stat-card:hover{border-color:rgba(255,255,255,0.15);transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,0.3)}
.stat-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.stat-label{font-size:12px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px}
.stat-icon{font-size:20px;padding:8px;background:rgba(255,255,255,0.03);border-radius:10px}
.stat-value{font-size:26px;font-weight:700;color:var(--text-primary);margin-bottom:4px;font-family:'JetBrains Mono',monospace}
.stat-sub{font-size:12px;color:var(--text-muted)}
.progress-bar{height:6px;background:rgba(255,255,255,0.05);border-radius:10px;margin-top:10px;overflow:hidden}
.progress-fill{height:100%;background:var(--mc-green);border-radius:10px;transition:width 0.5s ease}
.terminal-card{
background:var(--bg-terminal);border:1px solid var(--border);border-radius:var(--radius-lg);
overflow:hidden;box-shadow:0 12px 36px rgba(0,0,0,0.5);
}
.terminal-header{
background:var(--bg-secondary);padding:12px 20px;border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;
}
.mac-dots{display:flex;gap:8px}
.mac-dot{width:12px;height:12px;border-radius:50%}
.mac-dot.red{background:#ff5f56}
.mac-dot.yellow{background:#ffbd2e}
.mac-dot.green{background:#27c93f}
.terminal-title{font-size:13px;font-weight:600;color:var(--text-secondary);font-family:'JetBrains Mono',monospace}
.terminal-body{
padding:20px;font-family:'JetBrains Mono',monospace;font-size:13.5px;line-height:1.6;
color:#a7f3d0;height:420px;overflow-y:auto;white-space:pre-wrap;
}
.terminal-footer{
padding:14px 20px;background:var(--bg-secondary);border-top:1px solid var(--border);
display:flex;gap:12px;align-items:center;
}
.terminal-input{
flex:1;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);
padding:12px 16px;color:var(--text-primary);font-family:'JetBrains Mono',monospace;font-size:14px;outline:none;
}
.terminal-input:focus{border-color:var(--border-focus);box-shadow:0 0 0 3px rgba(34,197,94,0.1)}
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
padding:12px 22px;border-radius:var(--radius-sm);font-size:14px;font-weight:600;
border:none;cursor:pointer;transition:all 0.2s cubic-bezier(0.4,0,0.2,1);outline:none;
}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}
.btn-primary{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff;box-shadow:0 4px 16px rgba(34,197,94,0.3)}
.btn-primary:hover{box-shadow:0 6px 20px rgba(34,197,94,0.4)}
.btn-danger{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 4px 16px rgba(239,68,68,0.3)}
.btn-danger:hover{box-shadow:0 6px 20px rgba(239,68,68,0.4)}
.btn-secondary{background:var(--bg-card);border:1px solid var(--border);color:var(--text-primary)}
.btn-secondary:hover{background:var(--bg-card-hover);border-color:rgba(255,255,255,0.2)}
.btn-sm{padding:8px 14px;font-size:13px}
.action-bar{display:flex;gap:12px;margin-top:20px}
.file-grid{display:grid;grid-template-columns:300px 1fr;gap:20px;height:560px}
.file-sidebar{
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);
display:flex;flex-direction:column;overflow:hidden;
}
.file-list-header{padding:14px 18px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:13px;font-weight:600;color:var(--text-secondary)}
.file-list-content{flex:1;overflow-y:auto;padding:8px}
.file-row{
display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--radius-sm);
font-size:13.5px;color:var(--text-secondary);cursor:pointer;transition:all 0.15s;
}
.file-row:hover{background:var(--bg-card-hover);color:var(--text-primary)}
.file-row.active{background:rgba(34,197,94,0.12);color:var(--mc-green);font-weight:600}
.file-editor-wrap{
background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);
display:flex;flex-direction:column;overflow:hidden;
}
.editor-header{
padding:14px 20px;background:var(--bg-secondary);border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;
}
.editor-textarea{
flex:1;background:var(--bg-terminal);color:#f8fafc;font-family:'JetBrains Mono',monospace;
font-size:13.5px;line-height:1.7;padding:20px;border:none;outline:none;resize:none;
}
.card-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.panel-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}
.panel-header{padding:16px 20px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:14px;font-weight:600;color:var(--text-primary)}
.panel-body{padding:20px;display:flex;flex-direction:column;gap:16px}
.form-row{display:flex;align-items:center;justify-content:space-between;gap:14px}
.form-label{font-size:13.5px;color:var(--text-secondary);font-weight:500}
.form-input{
background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);
padding:8px 14px;color:var(--text-primary);font-size:13.5px;outline:none;width:220px;
}
.form-input:focus{border-color:var(--mc-green)}
.toggle{position:relative;display:inline-block;width:44px;height:24px}
.toggle input{opacity:0;width:0;height:0}
.slider{position:absolute;cursor:pointer;inset:0;background:var(--bg-input);border:1px solid var(--border);border-radius:24px;transition:.3s}
.slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:3px;background:var(--text-muted);border-radius:50%;transition:.3s}
input:checked + .slider{background:var(--mc-green);border-color:var(--mc-green)}
input:checked + .slider:before{transform:translateX(20px);background:#fff}
.toast-box{position:fixed;top:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:10px}
.toast{
background:var(--bg-secondary);border:1px solid var(--border);border-left:4px solid var(--mc-green);
padding:14px 20px;border-radius:var(--radius-md);box-shadow:0 10px 30px rgba(0,0,0,0.5);
font-size:13.5px;font-weight:500;display:flex;align-items:center;gap:12px;
animation:toastIn 0.3s cubic-bezier(0.4,0,0.2,1);
}
.toast.err{border-left-color:var(--mc-red)}
@keyframes toastIn{from{opacity:0;transform:translateX(50px)}to{opacity:1;transform:translateX(0)}}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:10px}
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.2)}
</style>
<div class="toast-box" id="toast-box"></div>
<div class="topbar">
<div class="topbar-left">
<div class="topbar-logo">โšก PTERODACTYL <span style="font-weight:400;color:var(--text-secondary)">MC</span></div>
<div class="topbar-badge">Purpur 1.21.1</div>
</div>
<div class="topbar-right">
<div class="status-pill stopped" id="nav-status">
<span class="status-dot stopped" id="nav-dot"></span>
<span id="nav-status-text">Server Stopped</span>
</div>
</div>
</div>
<div class="layout">
<div class="sidebar">
<div class="sidebar-title">Management</div>
<button class="nav-btn active" onclick="switchTab('console', this)">๐Ÿ–ฅ๏ธ Server Console</button>
<button class="nav-btn" onclick="switchTab('files', this)">๐Ÿ“ File Explorer</button>
<button class="nav-btn" style="color:var(--mc-blue);font-weight:600" onclick="switchTab('cloud', this)">โ˜๏ธ Dataset Live Sync</button>
<button class="nav-btn" onclick="switchTab('settings', this)">โš™๏ธ Configuration</button>
<button class="nav-btn" onclick="switchTab('users', this)">๐Ÿ‘ฅ Access Control</button>
</div>
<div class="main-content">
<!-- CONSOLE TAB -->
<div class="tab-pane active" id="tab-console">
<div class="page-header">
<div class="page-title">Console & Overview</div>
<div class="page-desc">Real-time resource utilization and terminal logs</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">Memory Usage</span>
<span class="stat-icon">๐Ÿง </span>
</div>
<div class="stat-value" id="stat-ram">0.0 / 4.0 GB</div>
<div class="stat-sub"><span id="stat-ram-pct">0%</span> allocated</div>
<div class="progress-bar"><div class="progress-fill" id="bar-ram" style="width:0%"></div></div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">CPU Utilization</span>
<span class="stat-icon">โšก</span>
</div>
<div class="stat-value" id="stat-cpu">0%</div>
<div class="stat-sub">Purpur Main Thread</div>
<div class="progress-bar"><div class="progress-fill" id="bar-cpu" style="width:0%;background:var(--mc-blue)"></div></div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">Online Players</span>
<span class="stat-icon">๐ŸŽฎ</span>
</div>
<div class="stat-value" id="stat-players">0</div>
<div class="stat-sub" id="stat-player-names">No players connected</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">System Uptime</span>
<span class="stat-icon">โฑ๏ธ</span>
</div>
<div class="stat-value" id="stat-uptime">0m</div>
<div class="stat-sub">Continuous execution</div>
</div>
</div>
<div class="terminal-card">
<div class="terminal-header">
<div class="mac-dots"><span class="mac-dot red"></span><span class="mac-dot yellow"></span><span class="mac-dot green"></span></div>
<span class="terminal-title">~/server/logs/latest.log</span>
<span style="font-size:12px;color:var(--mc-green)">โ— LIVE STREAM</span>
</div>
<div class="terminal-body" id="term-logs">[SYSTEM] Panel initialized. Ready to launch Minecraft Server...</div>
<div class="terminal-footer">
<span style="color:var(--mc-green);font-family:'JetBrains Mono'">โฏ</span>
<input type="text" class="terminal-input" id="term-cmd" placeholder="Type server command (e.g., say Hello, gamemode 1)..." onkeydown="if(event.key==='Enter') sendCommand()">
<button class="btn btn-primary btn-sm" onclick="sendCommand()">Send</button>
</div>
</div>
<div class="action-bar">
<button class="btn btn-primary" onclick="bridgeClick('gb-start')">โ–ถ Start Server</button>
<button class="btn btn-danger" onclick="bridgeClick('gb-stop')">๐Ÿ›‘ Stop Server</button>
<button class="btn btn-secondary" onclick="bridgeClick('gb-restart')">๐Ÿ”„ Restart</button>
</div>
</div>
<!-- FILES TAB -->
<div class="tab-pane" id="tab-files">
<div class="page-header">
<div class="page-title">File Explorer</div>
<div class="page-desc" id="path-display">๐Ÿ“ /root</div>
</div>
<div class="file-grid">
<div class="file-sidebar">
<div class="file-list-header">Directory Contents</div>
<div class="file-list-content" id="file-list-box">
<div class="file-row" style="color:var(--text-muted)">Loading files...</div>
</div>
<div style="padding:12px;border-top:1px solid var(--border);display:flex;gap:8px">
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="bridgeClick('gb-refresh-dir')">๐Ÿ”„ Refresh</button>
<button class="btn btn-primary btn-sm" style="flex:1" onclick="document.getElementById('hidden-upload').click()">๐Ÿ“ค Upload</button>
<input type="file" id="hidden-upload" multiple style="display:none" onchange="triggerUpload(this)">
</div>
</div>
<div class="file-editor-wrap">
<div class="editor-header">
<span id="editor-filename" style="font-family:'JetBrains Mono';font-size:14px;font-weight:600">Select a file to edit</span>
<div style="display:flex;gap:10px">
<button class="btn btn-secondary btn-sm" onclick="bridgeClick('gb-download-file')">โฌ‡ Download</button>
<button class="btn btn-primary btn-sm" onclick="saveCurrentFile()">๐Ÿ’พ Save Changes</button>
</div>
</div>
<textarea class="editor-textarea" id="editor-area" placeholder="File content will appear here..."></textarea>
</div>
</div>
<div class="card-grid" style="margin-top:20px">
<div class="panel-card">
<div class="panel-header">โž• Create Item</div>
<div class="panel-body">
<div class="form-row">
<input type="text" class="form-input" id="create-name-inp" placeholder="config.yml or folder_name" style="flex:1">
<select class="form-input" id="create-type-sel" style="width:120px">
<option value="File">๐Ÿ“„ File</option>
<option value="Folder">๐Ÿ“ Folder</option>
</select>
<button class="btn btn-primary btn-sm" onclick="createItemAction()">Create</button>
</div>
</div>
</div>
<div class="panel-card">
<div class="panel-header">๐Ÿ—‘๏ธ Delete / Rename</div>
<div class="panel-body">
<div class="form-row">
<input type="text" class="form-input" id="rename-new-inp" placeholder="New file name" style="flex:1">
<button class="btn btn-secondary btn-sm" onclick="renameItemAction()">Rename</button>
<button class="btn btn-danger btn-sm" onclick="deleteItemAction()">Delete Selected</button>
</div>
</div>
</div>
</div>
</div>
<!-- CLOUD SYNC TAB (DATASET DIRECT SYNC) -->
<div class="tab-pane" id="tab-cloud">
<div class="page-header">
<div class="page-title" style="color:var(--mc-blue)">โ˜๏ธ Dataset Live Sync (Server/ Folder)</div>
<div class="page-desc">ูŠุชู… ุฅู†ุดุงุก ุงู„ู€ Dataset ุชู„ู‚ุงุฆูŠุงู‹ ุนู†ุฏ ุฃูˆู„ ุชุดุบูŠู„ุŒ ูˆุชู†ุฒูŠู„ ูˆุฑูุน ูƒู„ ู…ุญุชูˆูŠุงุช ูˆู…ุฌู„ุฏุงุช ุงู„ุณูŠุฑูุฑ ู…ุจุงุดุฑุฉ ุฏุงุฎู„ ุงู„ู…ุฌู„ุฏ Server/</div>
</div>
<div class="card-grid">
<div class="panel-card" style="border-color:rgba(14,165,233,0.3)">
<div class="panel-header" style="background:rgba(14,165,233,0.1);color:#38bdf8">โ˜๏ธ Hugging Face Dataset Connection</div>
<div class="panel-body">
<div class="form-row">
<span class="form-label">HF Token (Write access)</span>
<input type="password" class="form-input" id="cloud-token" placeholder="hf_xxxxxxxxx">
</div>
<div class="form-row">
<span class="form-label">Dataset Repository ID</span>
<input type="text" class="form-input" id="cloud-repo" placeholder="username/minecraft-server">
</div>
<div class="form-row">
<span class="form-label">Auto Direct Sync (Server/)</span>
<label class="toggle"><input type="checkbox" id="cloud-auto-sync" checked><span class="slider"></span></label>
</div>
<div class="form-row">
<span class="form-label">Sync Interval (Mins)</span>
<input type="number" class="form-input" id="cloud-interval" value="10">
</div>
<div style="margin-top:10px;display:flex;justify-content:flex-end">
<button class="btn btn-primary" style="background:linear-gradient(135deg,#0ea5e9,#0284c7)" onclick="saveCloudConfigAction()">๐Ÿ’พ ุญูุธ ุฅุนุฏุงุฏุงุช ุงู„ุชุฒุงู…ู†</button>
</div>
</div>
</div>
<div class="panel-card">
<div class="panel-header">๐Ÿ”„ ุงู„ู…ุฒุงู…ู†ุฉ ุงู„ููˆุฑูŠุฉ ู…ุน ุงู„ู€ Dataset</div>
<div class="panel-body" style="gap:14px">
<div style="font-size:13.5px;color:var(--text-secondary);line-height:1.7;background:rgba(255,255,255,0.03);padding:14px;border-radius:var(--radius-sm);border:1px solid var(--border)">
โšก <b>ุงู„ุชุฒุงู…ู† ุงู„ู…ุจุงุดุฑ ูˆุงู„ุฅู†ุดุงุก ุงู„ุชู„ู‚ุงุฆูŠ:</b><br>
1. ุนู†ุฏ ุฃูˆู„ ุชุดุบูŠู„ุŒ ูŠุชุญู‚ู‚ ุงู„ู†ุธุงู… ู…ู† ูˆุฌูˆุฏ ุงู„ู€ Dataset ุนู„ู‰ ุญุณุงุจูƒุŒ ูˆุฅุฐุง ู„ู… ูŠูƒู† ู…ูˆุฌูˆุฏุงู‹ ูŠู‚ูˆู… <b>ุจุฅู†ุดุงุฆู‡ ุชู„ู‚ุงุฆูŠุงู‹</b> ูƒุฃุฑุดูŠู ู…ุบู„ู‚ (Private).<br>
2. ูŠุชู… ุชู†ุฒูŠู„ ูƒู„ ุงู„ู…ู„ูุงุช ุงู„ู…ูˆุฌูˆุฏุฉ ููŠ <code>[repo]/Server</code> ูˆุชุดุบูŠู„ ุงู„ุฎุงุฏู… ุนู„ูŠู‡ุง ู…ุจุงุดุฑุฉ.<br>
3. ุฃูŠ ู…ู„ู ุฃูˆ ู…ุฌู„ุฏ ุฌุฏูŠุฏ ูŠูู†ุดุฆู‡ ุงู„ุณูŠุฑูุฑ ูŠุชู… ุฏูุนู‡ ูˆู…ุฒุงู…ู†ุชูู‡ ููˆุฑุงู‹ ุฏุงุฎู„ <code>Server/</code> ู„ูŠูƒูˆู† ุงู„ู…ุณุชูˆุฏุน ุจู…ุซุงุจุฉ ูˆุญุฏุฉ ุชุฎุฒูŠู† ุณุญุงุจูŠุฉ ู…ุณุชู…ุฑุฉ ู„ู„ุณูŠุฑูุฑ.
</div>
<div style="display:flex;gap:12px;margin-top:10px">
<button class="btn btn-secondary" style="flex:1" onclick="bridgeClick('gb-cloud-restore')">๐Ÿ“ฅ ุชู†ุฒูŠู„ ูˆุณุญุจ ููˆุฑุงู‹ ู…ู† Server/</button>
<button class="btn btn-primary" style="flex:1;background:linear-gradient(135deg,#22c55e,#15803d)" onclick="bridgeClick('gb-cloud-backup')">๐Ÿ“ค ุฑูุน ูˆู…ุฒุงู…ู†ุฉ ููˆุฑุงู‹ ุฅู„ู‰ Server/</button>
</div>
</div>
</div>
</div>
</div>
<!-- SETTINGS TAB -->
<div class="tab-pane" id="tab-settings">
<div class="page-header">
<div class="page-title">Server Configuration</div>
<div class="page-desc">Modify core server.properties parameters directly</div>
</div>
<div class="card-grid">
<div class="panel-card">
<div class="panel-header">๐ŸŽฎ Gameplay Mechanics</div>
<div class="panel-body">
<div class="form-row">
<span class="form-label">Difficulty</span>
<select class="form-input" id="s-difficulty"><option>peaceful</option><option>easy</option><option selected>normal</option><option>hard</option></select>
</div>
<div class="form-row">
<span class="form-label">Default Gamemode</span>
<select class="form-input" id="s-gamemode"><option selected>survival</option><option>creative</option><option>adventure</option><option>spectator</option></select>
</div>
<div class="form-row">
<span class="form-label">Max Players</span>
<input type="number" class="form-input" id="s-max-players" value="20">
</div>
<div class="form-row">
<span class="form-label">View Distance</span>
<input type="number" class="form-input" id="s-view-distance" value="10">
</div>
</div>
</div>
<div class="panel-card">
<div class="panel-header">๐Ÿ”’ Network & Security</div>
<div class="panel-body">
<div class="form-row">
<span class="form-label">Online Mode (Cracked)</span>
<label class="toggle"><input type="checkbox" id="s-online-mode"><span class="slider"></span></label>
</div>
<div class="form-row">
<span class="form-label">Player vs Player (PvP)</span>
<label class="toggle"><input type="checkbox" id="s-pvp" checked><span class="slider"></span></label>
</div>
<div class="form-row">
<span class="form-label">Spawn Monsters</span>
<label class="toggle"><input type="checkbox" id="s-spawn-monsters" checked><span class="slider"></span></label>
</div>
<div class="form-row">
<span class="form-label">Enable Whitelist</span>
<label class="toggle"><input type="checkbox" id="s-whitelist"><span class="slider"></span></label>
</div>
</div>
</div>
</div>
<div style="margin-top:20px;display:flex;justify-content:flex-end;gap:12px">
<button class="btn btn-secondary" onclick="bridgeClick('gb-load-settings')">Reload from Disk</button>
<button class="btn btn-primary" onclick="saveSettingsAction()">๐Ÿ’พ Save Properties</button>
</div>
</div>
<!-- USERS TAB -->
<div class="tab-pane" id="tab-users">
<div class="page-header">
<div class="page-title">Access Control (RBAC)</div>
<div class="page-desc">Manage panel collaborators and granular permissions</div>
</div>
<div class="card-grid">
<div class="panel-card">
<div class="panel-header">โž• Assign Collaborator</div>
<div class="panel-body">
<input type="text" class="form-input" id="usr-name" placeholder="Username..." style="width:100%">
<select class="form-input" id="usr-role" style="width:100%">
<option value="editor">Editor (Console + Files + Cloud)</option>
<option value="admin">Administrator (Full Access)</option>
<option value="viewer">Viewer (Read Only)</option>
</select>
<button class="btn btn-primary" onclick="addUserAction()">Add Collaborator</button>
</div>
</div>
<div class="panel-card">
<div class="panel-header">๐Ÿ“‹ Authorized Users List</div>
<div class="panel-body" id="users-list-area" style="font-size:14px;line-height:1.8">No users configured.</div>
</div>
</div>
</div>
</div>
</div>
"""
DASHBOARD_JS = r"""
function showToast(msg, isErr=false) {
var box = document.getElementById('toast-box');
if(!box) return;
var t = document.createElement('div');
t.className = 'toast' + (isErr ? ' err' : '');
t.innerHTML = (isErr ? 'โŒ ' : 'โœ… ') + msg;
box.appendChild(t);
setTimeout(function(){ t.remove() }, 3500);
}
function switchTab(name, btn) {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
if(btn) btn.classList.add('active');
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
var pane = document.getElementById('tab-' + name);
if(pane) pane.classList.add('active');
if(name === 'files') bridgeClick('gb-refresh-dir');
if(name === 'cloud') bridgeClick('gb-load-cloud-config');
if(name === 'settings') bridgeClick('gb-load-settings');
if(name === 'users') bridgeClick('gb-refresh-users');
}
function bridgeClick(id) {
var el = document.getElementById(id);
if(el){
var btn = el.querySelector('button') || el;
btn.click();
}
}
function setGradioInput(id, val) {
var root = document.getElementById(id);
if(!root) return;
var inp = root.querySelector('textarea, input');
if(!inp) return;
var proto = inp instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(inp, val);
inp.dispatchEvent(new Event('input', { bubbles: true }));
}
function getGradioText(id) {
var root = document.getElementById(id);
if(!root) return '';
var ta = root.querySelector('textarea, input');
return ta ? ta.value : (root.innerText || '');
}
var _selectedFile = '';
var _selectedFolder = '';
function selectFolderAction(fName) {
_selectedFolder = fName;
setGradioInput('gv-folder', fName);
bridgeClick('gb-enter-folder');
}
function selectFileAction(fName) {
_selectedFile = fName;
document.getElementById('editor-filename').textContent = 'Editing: ' + fName;
var ren = document.getElementById('rename-new-inp');
if(ren) ren.value = fName;
setGradioInput('gv-file', fName);
bridgeClick('gb-select-file');
}
function saveCurrentFile() {
if(!_selectedFile) return showToast('No file selected!', true);
var val = document.getElementById('editor-area').value;
setGradioInput('gv-content', val);
bridgeClick('gb-save-file');
}
function createItemAction() {
var name = document.getElementById('create-name-inp').value;
var type = document.getElementById('create-type-sel').value;
if(!name) return showToast('Enter name first!', true);
setGradioInput('gv-name', name);
setGradioInput('gv-type', type);
bridgeClick('gb-create');
}
function renameItemAction() {
var newN = document.getElementById('rename-new-inp').value;
if(!_selectedFile || !newN) return showToast('Select a file and enter new name!', true);
setGradioInput('gv-old', _selectedFile);
setGradioInput('gv-name', newN);
bridgeClick('gb-rename');
}
function deleteItemAction() {
if(!_selectedFile) return showToast('Select a file to delete!', true);
setGradioInput('gv-old', _selectedFile);
bridgeClick('gb-delete');
_selectedFile = '';
}
function triggerUpload(inp) {
if(!inp.files.length) return;
var dt = new DataTransfer();
for(var i=0; i<inp.files.length; i++) dt.items.add(inp.files[i]);
var hidden = document.querySelector('#gv-upload input[type="file"]');
if(hidden){
var proto = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files').set;
proto.call(hidden, dt.files);
hidden.dispatchEvent(new Event('change', { bubbles: true }));
bridgeClick('gb-upload');
showToast('Uploading files...');
}
}
function sendCommand() {
var inp = document.getElementById('term-cmd');
if(!inp || !inp.value.trim()) return;
setGradioInput('gv-cmd', inp.value.trim());
bridgeClick('gb-send-cmd');
inp.value = '';
}
function saveSettingsAction() {
var props = {
'difficulty': document.getElementById('s-difficulty').value,
'gamemode': document.getElementById('s-gamemode').value,
'max-players': document.getElementById('s-max-players').value,
'view-distance': document.getElementById('s-view-distance').value,
'online-mode': document.getElementById('s-online-mode').checked ? 'true' : 'false',
'pvp': document.getElementById('s-pvp').checked ? 'true' : 'false',
'spawn-monsters': document.getElementById('s-spawn-monsters').checked ? 'true' : 'false',
'white-list': document.getElementById('s-whitelist').checked ? 'true' : 'false'
};
setGradioInput('gv-settings-data', JSON.stringify(props));
bridgeClick('gb-save-settings');
}
function saveCloudConfigAction() {
var cfg = {
'hf_token': document.getElementById('cloud-token').value,
'hf_dataset_repo': document.getElementById('cloud-repo').value,
'auto_sync': document.getElementById('cloud-auto-sync').checked,
'sync_interval': document.getElementById('cloud-interval').value
};
setGradioInput('gv-cloud-config-data', JSON.stringify(cfg));
bridgeClick('gb-save-cloud-config');
}
function addUserAction() {
var u = document.getElementById('usr-name').value;
var r = document.getElementById('usr-role').value;
if(!u) return showToast('Enter username!', true);
setGradioInput('gv-uname', u);
setGradioInput('gv-role', r);
bridgeClick('gb-add-user');
}
var _lastConsole = '';
var _lastOp = '';
var _pollCount = 0;
function pollSystem() {
_pollCount++;
var st = getGradioText('g-status');
var isRun = st.indexOf('Running') >= 0;
var pill = document.getElementById('nav-status');
if(pill) {
pill.className = 'status-pill ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-dot').className = 'status-dot ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-status-text').textContent = isRun ? 'Server Online' : 'Server Stopped';
}
var con = getGradioText('g-console');
if(con && con !== _lastConsole) {
_lastConsole = con;
var term = document.getElementById('term-logs');
if(term) { term.textContent = con; term.scrollTop = term.scrollHeight; }
}
var resTxt = getGradioText('g-resources');
if(resTxt){
try {
var r = JSON.parse(resTxt);
document.getElementById('stat-ram').textContent = r.ram_used + ' / ' + r.ram_total + ' GB';
document.getElementById('stat-ram-pct').textContent = r.ram_pct + '%';
document.getElementById('bar-ram').style.width = r.ram_pct + '%';
document.getElementById('stat-cpu').textContent = r.cpu + '%';
document.getElementById('bar-cpu').style.width = Math.min(r.cpu, 100) + '%';
document.getElementById('stat-uptime').textContent = r.uptime;
} catch(e){}
}
var pl = getGradioText('g-players');
if(pl && pl !== 'No players online') {
var arr = pl.split(',').filter(Boolean);
document.getElementById('stat-players').textContent = arr.length;
document.getElementById('stat-player-names').textContent = pl;
} else {
document.getElementById('stat-players').textContent = '0';
document.getElementById('stat-player-names').textContent = 'No players connected';
}
var op = getGradioText('g-op');
if(op && op !== _lastOp) {
_lastOp = op;
showToast(op, op.indexOf('โŒ') >= 0 || op.indexOf('โ›”') >= 0);
}
var fld = getGradioText('g-folders');
var fil = getGradioText('g-files');
if(fld || fil) {
try {
var fldArr = fld ? JSON.parse(fld) : [];
var filArr = fil ? JSON.parse(fil) : [];
var html = '';
fldArr.forEach(function(f){ html += '<div class="file-row" onclick="selectFolderAction(\''+f+'\')">๐Ÿ“ '+f+'</div>'; });
filArr.forEach(function(f){ html += '<div class="file-row" onclick="selectFileAction(\''+f+'\')">๐Ÿ“„ '+f+'</div>'; });
var box = document.getElementById('file-list-box');
if(box) box.innerHTML = html || '<div class="file-row" style="color:var(--text-muted)">Empty directory</div>';
} catch(e){}
}
var edContent = getGradioText('g-editor');
if(edContent && document.activeElement !== document.getElementById('editor-area')) {
document.getElementById('editor-area').value = edContent;
}
var pDisplay = getGradioText('g-path');
if(pDisplay) document.getElementById('path-display').textContent = pDisplay;
var usrList = getGradioText('g-users');
if(usrList) document.getElementById('users-list-area').innerHTML = usrList.replace(/\n/g, '<br>');
var cCfg = getGradioText('g-cloud-config');
if(cCfg && document.activeElement !== document.getElementById('cloud-token') && document.activeElement !== document.getElementById('cloud-repo')) {
try {
var cfg = JSON.parse(cCfg);
if(cfg.hf_token) document.getElementById('cloud-token').value = cfg.hf_token;
if(cfg.hf_dataset_repo) document.getElementById('cloud-repo').value = cfg.hf_dataset_repo;
document.getElementById('cloud-auto-sync').checked = cfg.auto_sync !== false;
if(cfg.sync_interval) document.getElementById('cloud-interval').value = cfg.sync_interval;
} catch(e){}
}
bridgeClick('gb-refresh-con');
if(_pollCount % 2 === 0) bridgeClick('gb-periodic-status');
if(_pollCount % 3 === 0) bridgeClick('gb-refresh-res');
if(_pollCount % 4 === 0) bridgeClick('gb-refresh-players');
}
setTimeout(function(){
setInterval(pollSystem, 1500);
}, 500);
"""
with gr.Blocks(title="Pterodactyl MC Panel", css="#bridge-wrap{display:none!important}", head="<script>\n" + DASHBOARD_JS + "\n</script>") as demo:
current_path_state, file_path_state, load_offset_state, current_user = gr.State(""), gr.State(""), gr.State(0), gr.State(None)
gr.HTML(value=DASHBOARD_HTML)
with gr.Column(elem_id="bridge-wrap"):
_console = gr.Textbox(elem_id="g-console")
_status = gr.Textbox(elem_id="g-status")
_path = gr.Textbox(elem_id="g-path")
_folders = gr.Textbox(elem_id="g-folders")
_files = gr.Textbox(elem_id="g-files")
_op = gr.Textbox(elem_id="g-op")
_editor = gr.Textbox(elem_id="g-editor")
_editor_title = gr.Textbox(elem_id="g-editor-title")
_editor_fpath = gr.Textbox(elem_id="g-editor-fpath")
_editor_offset = gr.Textbox(elem_id="g-editor-offset")
_editor_has_more = gr.Textbox(elem_id="g-editor-has-more")
_users = gr.Textbox(elem_id="g-users")
_resources = gr.Textbox(elem_id="g-resources")
_players = gr.Textbox(elem_id="g-players")
_cmd_result = gr.Textbox(elem_id="g-cmd-result")
_download_data = gr.Textbox(elem_id="g-download")
_settings = gr.Textbox(elem_id="g-settings")
_cloud_config_json = gr.Textbox(elem_id="g-cloud-config")
_b_start = gr.Button(elem_id="gb-start")
_b_stop = gr.Button(elem_id="gb-stop")
_b_restart = gr.Button(elem_id="gb-restart")
_b_refresh_con = gr.Button(elem_id="gb-refresh-con")
_b_refresh_dir = gr.Button(elem_id="gb-refresh-dir")
_b_enter_folder = gr.Button(elem_id="gb-enter-folder")
_b_select_file = gr.Button(elem_id="gb-select-file")
_b_save_file = gr.Button(elem_id="gb-save-file")
_b_create = gr.Button(elem_id="gb-create")
_b_rename = gr.Button(elem_id="gb-rename")
_b_delete = gr.Button(elem_id="gb-delete")
_b_upload = gr.Button(elem_id="gb-upload")
_b_add_user = gr.Button(elem_id="gb-add-user")
_b_refresh_users = gr.Button(elem_id="gb-refresh-users")
_b_periodic_status = gr.Button(elem_id="gb-periodic-status")
_b_send_cmd = gr.Button(elem_id="gb-send-cmd")
_b_refresh_res = gr.Button(elem_id="gb-refresh-res")
_b_refresh_players = gr.Button(elem_id="gb-refresh-players")
_b_download_file = gr.Button(elem_id="gb-download-file")
_b_load_settings = gr.Button(elem_id="gb-load-settings")
_b_save_settings = gr.Button(elem_id="gb-save-settings")
_b_cloud_restore = gr.Button(elem_id="gb-cloud-restore")
_b_cloud_backup = gr.Button(elem_id="gb-cloud-backup")
_b_load_cloud_config = gr.Button(elem_id="gb-load-cloud-config")
_b_save_cloud_config = gr.Button(elem_id="gb-save-cloud-config")
_v_folder = gr.Textbox(elem_id="gv-folder")
_v_file = gr.Textbox(elem_id="gv-file")
_v_content = gr.Textbox(elem_id="gv-content")
_v_name = gr.Textbox(elem_id="gv-name")
_v_type = gr.Textbox(elem_id="gv-type", value="File")
_v_old = gr.Textbox(elem_id="gv-old")
_v_del_folder = gr.Checkbox(elem_id="gv-del-folder", value=False)
_v_uname = gr.Textbox(elem_id="gv-uname")
_v_role = gr.Textbox(elem_id="gv-role", value="editor")
_v_upload = gr.File(elem_id="gv-upload", file_count="multiple")
_v_cmd = gr.Textbox(elem_id="gv-cmd")
_v_download_path = gr.Textbox(elem_id="gv-download-path")
_v_settings_data = gr.Textbox(elem_id="gv-settings-data")
_v_cloud_config_data = gr.Textbox(elem_id="gv-cloud-config-data")
async def w_scan_directory(rel):
target = get_safe_path(rel)
folders, files = [], []
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target))), timeout=5.0)
for item in entries:
fp = os.path.join(target, item)
try:
if await asyncio.to_thread(os.path.isdir, fp): folders.append(item)
elif os.path.splitext(item)[1].lower() not in BINARY_EXTENSIONS: files.append(item)
except: files.append(item)
except: pass
folders.sort(); files.sort()
return f"๐Ÿ“ /root/{rel}" if rel else "๐Ÿ“ /root", json.dumps(folders), json.dumps(files)
async def w_enter_folder(folder, rel):
if rel is None: rel = ""
if not folder: return rel, *await w_scan_directory(rel)
if folder == ".. (ุงู„ู…ุฌู„ุฏ ุงู„ุฃุนู„ู‰)":
parent = "/".join(rel.strip("/").split("/")[:-1]) if rel else ""
return parent, *await w_scan_directory(parent)
new_rel = os.path.join(rel, folder).strip("/")
return new_rel, *await w_scan_directory(new_rel)
async def w_load_file(file, rel):
r = await load_file_content(file, rel)
content = r[0].get("value", "") if isinstance(r[0], dict) else r[0]
title = r[1].get("value", "") if isinstance(r[1], dict) else r[1]
has_more = r[6].get("visible", False) if isinstance(r[6], dict) else False
return content, title, str(r[4]), str(r[5]), str(has_more)
async def w_create(name, typ, rel, user):
r = await create_item(name, typ, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_rename(old, new, rel, user):
r = await rename_item(old, new, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_delete(name, is_folder, rel, user):
r = await delete_item(name, is_folder, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_upload(files, rel, user):
r = await upload_to_current_dir(files, rel, user)
return r[0], *await w_scan_directory(rel)
def w_list_users():
users = load_users()
if not users: return "No users configured yet."
lines = []
for u, r in sorted(users.items()):
icon = {"admin": "๐Ÿ›ก๏ธ", "editor": "โœ๏ธ", "viewer": "๐Ÿ‘๏ธ"}.get(r, "โ“")
lines.append(f"<b>{icon} {u}</b> &mdash; <span style='color:var(--text-secondary)'>{r.upper()}</span>")
return "<br>".join(lines)
def w_add_user(name, role, user):
if not has_perm(user, "users_manage"): return "โ›” Only admins can manage users"
if not name: return "โš ๏ธ Enter a username"
users = load_users(); users[name.strip()] = role; save_users(users)
return f"โœ… Added {name.strip()} as {role}"
def w_send_cmd(cmd, user): return send_console_command(cmd, user)
def w_get_resources(): return json.dumps(get_system_resources())
def w_get_players(): return ", ".join(get_online_players()) or "No players online"
def w_download_file(filepath, rel, user):
if not has_perm(user, "files_read"): return "ERROR:Permission denied"
if not filepath: return ""
target = get_safe_path(os.path.join(rel, filepath) if rel else filepath)
if not os.path.isfile(target): return ""
try:
with open(target, "rb") as f:
data = base64.b64encode(f.read()).decode()
return f"{filepath}|{data}"
except: return ""
def w_load_settings():
props_path = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_path): return "{}"
try:
props = {}
with open(props_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1); props[k.strip()] = v.strip()
return json.dumps(props)
except: return "{}"
def w_save_settings(data_str, user):
if not has_perm(user, "files_write"): return "โ›” Permission denied"
props_path = os.path.join(WORKDIR, "server.properties")
try:
props = json.loads(data_str)
with open(props_path, "w") as f:
for k, v in sorted(props.items()): f.write(f"{k}={v}\n")
return "โœ… Settings saved successfully!"
except Exception as e: return f"โŒ Error saving: {e}"
def w_load_cloud_config():
return json.dumps(load_hf_config())
def w_save_cloud_config(data_str, user):
if not has_perm(user, "cloud_manage"): return "โ›” You don't have permission to manage cloud sync"
try:
cfg = json.loads(data_str)
success, msg = save_hf_config(cfg)
return msg
except Exception as e: return f"โŒ Error saving cloud config: {e}"
def w_cloud_restore(user):
if not has_perm(user, "cloud_manage"): return "โ›” You don't have permission to perform cloud restore"
success, msg = init_dataset_and_pull()
return msg
def w_cloud_backup(user):
if not has_perm(user, "cloud_manage"): return "โ›” You don't have permission to perform cloud backup"
success, msg = push_to_dataset()
return msg
_b_start.click(fn=control_server, inputs=[gr.State("START"), current_user], outputs=_status)
_b_stop.click(fn=control_server, inputs=[gr.State("STOP"), current_user], outputs=_status)
_b_restart.click(fn=control_server, inputs=[gr.State("RESTART"), current_user], outputs=_status)
_b_refresh_con.click(fn=get_logs, outputs=_console)
_b_refresh_dir.click(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
_b_enter_folder.click(fn=w_enter_folder, inputs=[_v_folder, current_path_state], outputs=[current_path_state, _path, _folders, _files])
_b_select_file.click(fn=w_load_file, inputs=[_v_file, current_path_state], outputs=[_editor, _editor_title, _editor_fpath, _editor_offset, _editor_has_more])
_b_save_file.click(fn=save_file_content, inputs=[_v_file, _v_content, current_path_state, current_user], outputs=_op)
_b_create.click(fn=w_create, inputs=[_v_name, _v_type, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_rename.click(fn=w_rename, inputs=[_v_old, _v_name, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_delete.click(fn=w_delete, inputs=[_v_old, _v_del_folder, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_upload.click(fn=w_upload, inputs=[_v_upload, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_add_user.click(fn=w_add_user, inputs=[_v_uname, _v_role, current_user], outputs=_op)
_b_refresh_users.click(fn=w_list_users, outputs=_users)
_b_periodic_status.click(fn=get_server_status, outputs=_status)
_b_send_cmd.click(fn=w_send_cmd, inputs=[_v_cmd, current_user], outputs=_cmd_result)
_b_refresh_res.click(fn=w_get_resources, outputs=_resources)
_b_refresh_players.click(fn=w_get_players, outputs=_players)
_b_download_file.click(fn=w_download_file, inputs=[_v_download_path, current_path_state, current_user], outputs=_download_data)
_b_load_settings.click(fn=w_load_settings, outputs=_settings)
_b_save_settings.click(fn=w_save_settings, inputs=[_v_settings_data, current_user], outputs=_cmd_result)
_b_cloud_restore.click(fn=w_cloud_restore, inputs=[current_user], outputs=_op)
_b_cloud_backup.click(fn=w_cloud_backup, inputs=[current_user], outputs=_op)
_b_load_cloud_config.click(fn=w_load_cloud_config, outputs=_cloud_config_json)
_b_save_cloud_config.click(fn=w_save_cloud_config, inputs=[_v_cloud_config_data, current_user], outputs=_op)
demo.load(fn=get_logs, outputs=_console)
demo.load(fn=get_server_status, outputs=_status)
demo.load(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
demo.load(fn=w_list_users, outputs=_users)
demo.load(fn=w_load_cloud_config, outputs=_cloud_config_json)
demo.queue(default_concurrency_limit=3)
log("[SYSTEM] Pterodactyl MC Panel initialized with Live Dataset Sync.")
demo.launch()