| 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: |
| |
| subprocess.run(["pip", "install", "psutil"], capture_output=True) |
| try: |
| import psutil |
| except ImportError: |
| psutil = None |
|
|
| try: |
| import huggingface_hub |
| except ImportError: |
| |
| 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_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) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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_lock = threading.Lock() |
|
|
| start_time = time.time() |
| _last_auto_sync_time = time.time() |
|
|
| |
| 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) |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| total_gb = cgroup_gb if cgroup_gb else psutil_gb |
|
|
| |
| safe_max_gb = max(1, int(total_gb * 0.5)) |
|
|
| |
| |
| 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: |
| |
| init_dataset_and_pull() |
|
|
| |
| download_dependencies() |
|
|
| |
| push_to_dataset() |
|
|
| |
| 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") |
|
|
| |
| 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: |
| |
| 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.") |
| |
| |
| 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) |
|
|
| |
| 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") |
|
|
| |
| 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> — <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() |