From d188cec1f0b367c35e2f3dfc1603455d1006e1a2 Mon Sep 17 00:00:00 2001 From: arkonsadter Date: Fri, 16 Jan 2026 18:56:21 +0600 Subject: [PATCH] Added Daemon system and fixed interface --- DAEMON_SETUP.md | 264 + backend/daemons.py | 331 ++ backend/data/daemons.json | 10 + backend/main.py | 113 +- backend/users.json | 23 +- daemon/.env | 11 + daemon/.env.example | 20 + daemon/README.md | 195 + daemon/install.bat | 9 + daemon/main.py | 307 ++ daemon/requirements.txt | 5 + daemon/start.bat | 4 + frontend/src/App.jsx | 46 +- frontend/src/components/CreateServerModal.jsx | 80 +- frontend/src/components/Daemons.jsx | 382 ++ key.py | 2 + logs_Arkon_NeveTimePanel_1_1_2.txt | 4912 ----------------- logs_Arkon_NeveTimePanel_3_2_3.txt | 20 - servers/sdfsfsf/dfgdg/.gitkeep | 0 servers/sdfsfsf/dfgdg/sdff.txt | 0 servers/sdfsfsf/panel_config.json | 7 + servers/sfsf/panel_config.json | 6 + users.json | 38 + ОБНОВЛЕНИЯ.md | 174 + 24 files changed, 1974 insertions(+), 4985 deletions(-) create mode 100644 DAEMON_SETUP.md create mode 100644 backend/daemons.py create mode 100644 backend/data/daemons.json create mode 100644 daemon/.env create mode 100644 daemon/.env.example create mode 100644 daemon/README.md create mode 100644 daemon/install.bat create mode 100644 daemon/main.py create mode 100644 daemon/requirements.txt create mode 100644 daemon/start.bat create mode 100644 frontend/src/components/Daemons.jsx create mode 100644 key.py delete mode 100644 logs_Arkon_NeveTimePanel_1_1_2.txt delete mode 100644 logs_Arkon_NeveTimePanel_3_2_3.txt create mode 100644 servers/sdfsfsf/dfgdg/.gitkeep create mode 100644 servers/sdfsfsf/dfgdg/sdff.txt create mode 100644 servers/sdfsfsf/panel_config.json create mode 100644 servers/sfsf/panel_config.json create mode 100644 users.json create mode 100644 ОБНОВЛЕНИЯ.md diff --git a/DAEMON_SETUP.md b/DAEMON_SETUP.md new file mode 100644 index 0000000..f26aa77 --- /dev/null +++ b/DAEMON_SETUP.md @@ -0,0 +1,264 @@ +# Настройка системы демонов MC Panel + +## Что такое демоны? + +Демоны (Daemons) - это удаленные серверы, на которых можно запускать Minecraft серверы. Система демонов позволяет: + +- Распределять серверы по разным физическим машинам +- Масштабировать инфраструктуру +- Управлять серверами на разных локациях из одной панели +- Балансировать нагрузку между серверами + +## Быстрый старт + +### 1. Установка демона на удаленный сервер + +#### Windows: +```bash +# 1. Скопируйте папку daemon на удаленный сервер +# 2. Откройте командную строку в папке daemon +# 3. Установите зависимости +install.bat + +# 4. Настройте .env файл +copy .env.example .env +notepad .env + +# 5. Запустите демон +start.bat +``` + +#### Linux: +```bash +# 1. Скопируйте папку daemon на удаленный сервер +# 2. Установите зависимости +cd daemon +pip install -r requirements.txt + +# 3. Настройте .env файл +cp .env.example .env +nano .env + +# 4. Запустите демон +python main.py +``` + +### 2. Настройка .env файла демона + +```env +# Уникальный ID демона +DAEMON_ID=daemon-1 + +# Отображаемое имя +DAEMON_NAME=Main Server + +# Порт для API +DAEMON_PORT=24444 + +# Секретный ключ (сгенерируйте случайный) +DAEMON_KEY=your-secret-key-here + +# Директория для серверов +SERVERS_DIR=./servers +``` + +**Важно:** Сгенерируйте надежный ключ: +```python +import secrets +print(secrets.token_urlsafe(32)) +``` + +### 3. Подключение демона к панели + +1. Откройте основную панель управления +2. Войдите как владелец (owner) или администратор (admin) +3. В боковом меню нажмите "Демоны" (иконка сервера) +4. Нажмите "Добавить демон" +5. Заполните форму: + - **Название**: Main Server (или любое другое) + - **IP адрес**: IP адрес сервера с демоном + - **Порт**: 24444 (или ваш порт из .env) + - **Ключ демона**: ваш DAEMON_KEY из .env + - **Примечания**: дополнительная информация (необязательно) +6. Нажмите "Добавить" + +### 4. Проверка подключения + +После добавления демон должен отображаться со статусом "Онлайн" (зеленый индикатор). + +Вы увидите: +- Статус демона (онлайн/оффлайн) +- Использование CPU, ОЗУ и диска +- Количество серверов на демоне + +## Архитектура + +``` +┌─────────────────┐ +│ Основная панель │ (порт 8000) +│ (Frontend + │ +│ Backend) │ +└────────┬─────────┘ + │ + │ HTTP API + │ + ┌────┴────┬────────┬────────┐ + │ │ │ │ +┌───▼───┐ ┌──▼───┐ ┌──▼───┐ ┌──▼───┐ +│Daemon1│ │Daemon2│ │Daemon3│ │... │ +│(24444)│ │(24444)│ │(24444)│ │ │ +└───┬───┘ └──┬───┘ └──┬───┘ └──────┘ + │ │ │ +┌───▼───┐ ┌──▼───┐ ┌──▼───┐ +│Server1│ │Server2│ │Server3│ +│Server2│ │Server3│ │Server4│ +└───────┘ └──────┘ └──────┘ +``` + +## Безопасность + +### 1. Файрвол + +Настройте файрвол, чтобы разрешить доступ к порту демона только с IP основной панели: + +#### Windows (PowerShell): +```powershell +New-NetFirewallRule -DisplayName "MC Panel Daemon" -Direction Inbound -LocalPort 24444 -Protocol TCP -Action Allow -RemoteAddress "IP_ПАНЕЛИ" +``` + +#### Linux (ufw): +```bash +sudo ufw allow from IP_ПАНЕЛИ to any port 24444 +``` + +#### Linux (iptables): +```bash +sudo iptables -A INPUT -p tcp -s IP_ПАНЕЛИ --dport 24444 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 24444 -j DROP +``` + +### 2. HTTPS (рекомендуется для продакшена) + +Используйте reverse proxy (nginx) с SSL сертификатом: + +```nginx +server { + listen 443 ssl http2; + server_name daemon.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:24444; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### 3. Сильные ключи + +- Используйте случайные ключи длиной минимум 32 символа +- Не используйте одинаковые ключи для разных демонов +- Храните ключи в безопасности + +## Запуск как сервис + +### Linux (systemd) + +1. Создайте файл `/etc/systemd/system/mcpanel-daemon.service`: + +```ini +[Unit] +Description=MC Panel Daemon +After=network.target + +[Service] +Type=simple +User=mcpanel +WorkingDirectory=/path/to/daemon +ExecStart=/usr/bin/python3 /path/to/daemon/main.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +2. Запустите сервис: + +```bash +sudo systemctl enable mcpanel-daemon +sudo systemctl start mcpanel-daemon +sudo systemctl status mcpanel-daemon +``` + +### Windows (NSSM) + +1. Скачайте NSSM: https://nssm.cc/download +2. Установите сервис: + +```cmd +nssm install MCPanelDaemon "C:\Python\python.exe" "C:\path\to\daemon\main.py" +nssm set MCPanelDaemon AppDirectory "C:\path\to\daemon" +nssm start MCPanelDaemon +``` + +## Управление серверами на демонах + +После подключения демона вы можете: + +1. **Создавать серверы** - при создании сервера можно будет выбрать демон +2. **Просматривать статистику** - CPU, ОЗУ, диск каждого демона +3. **Управлять серверами** - запуск, остановка, консоль, файлы +4. **Мониторить состояние** - статус демонов обновляется автоматически + +## Troubleshooting + +### Демон показывает статус "Оффлайн" + +1. Проверьте, что демон запущен на удаленном сервере +2. Проверьте файрвол и порты +3. Проверьте, что ключ в панели совпадает с DAEMON_KEY +4. Проверьте IP адрес и порт +5. Проверьте логи демона + +### Ошибка "Connection error" + +- Проверьте сетевое подключение между панелью и демоном +- Проверьте, что порт не заблокирован файрволом +- Попробуйте подключиться вручную: `curl http://IP:24444/api/status` + +### Ошибка "Invalid daemon key" + +- Проверьте, что ключ в панели точно совпадает с DAEMON_KEY в .env +- Убедитесь, что нет лишних пробелов или символов +- Перезапустите демон после изменения .env + +## Мониторинг + +Демоны автоматически отправляют информацию о: +- Использовании CPU +- Использовании ОЗУ +- Использовании диска +- Количестве серверов +- Количестве запущенных серверов + +Эта информация обновляется каждые 10 секунд в интерфейсе панели. + +## Масштабирование + +Вы можете добавить неограниченное количество демонов: + +1. Установите демон на новый сервер +2. Используйте уникальный DAEMON_ID для каждого демона +3. Добавьте демон в панель +4. Распределяйте серверы между демонами + +## Поддержка + +Если у вас возникли проблемы: +1. Проверьте логи демона +2. Проверьте логи основной панели +3. Создайте тикет в системе поддержки diff --git a/backend/daemons.py b/backend/daemons.py new file mode 100644 index 0000000..5e4d173 --- /dev/null +++ b/backend/daemons.py @@ -0,0 +1,331 @@ +""" +Управление демонами (удаленными серверами) +""" + +from fastapi import APIRouter, HTTPException, Depends, Header +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from pydantic import BaseModel +from typing import List, Optional +import json +import httpx +from pathlib import Path +from jose import JWTError, jwt + +router = APIRouter() +security = HTTPBearer(auto_error=False) + +# Файл с конфигурацией демонов +DAEMONS_FILE = Path("backend/data/daemons.json") +DAEMONS_FILE.parent.mkdir(exist_ok=True) + +# Файл с пользователями - проверяем оба возможных пути +USERS_FILE = Path("backend/users.json") if Path("backend/users.json").exists() else Path("users.json") + +# Настройки JWT (должны совпадать с main.py) +SECRET_KEY = "your-secret-key-change-this-in-production-12345" +ALGORITHM = "HS256" + + +def load_users_dict(): + """Загрузить пользователей из файла""" + if USERS_FILE.exists(): + with open(USERS_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return {} + + +def get_current_user_from_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + """Получить текущего пользователя из токена""" + if not credentials: + raise HTTPException(status_code=401, detail="Требуется авторизация") + + token = credentials.credentials + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + + if username is None: + raise HTTPException(status_code=401, detail="Неверный токен") + + # Пытаемся получить роль из токена + role = payload.get("role") + + print(f"[DEBUG] Username from token: {username}") + print(f"[DEBUG] Role from token: {role}") + + # Если роли нет в токене, загружаем из базы + if not role: + print(f"[DEBUG] Role not in token, loading from database...") + print(f"[DEBUG] USERS_FILE path: {USERS_FILE}") + print(f"[DEBUG] USERS_FILE exists: {USERS_FILE.exists()}") + + users = load_users_dict() + print(f"[DEBUG] Loaded users: {list(users.keys())}") + + if username not in users: + raise HTTPException(status_code=401, detail="Пользователь не найден") + role = users[username].get("role", "user") + print(f"[DEBUG] Role from database: {role}") + + print(f"[DEBUG] Final role: {role}") + return {"username": username, "role": role} + except JWTError as e: + print(f"[DEBUG] JWT Error: {e}") + raise HTTPException(status_code=401, detail="Неверный токен") + + +class DaemonCreate(BaseModel): + name: str + address: str + port: int + key: str + remarks: Optional[str] = "" + + +class DaemonUpdate(BaseModel): + name: Optional[str] = None + address: Optional[str] = None + port: Optional[int] = None + key: Optional[str] = None + remarks: Optional[str] = None + + +def load_daemons(): + """Загрузить список демонов""" + if not DAEMONS_FILE.exists(): + return {} + + with open(DAEMONS_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + + +def save_daemons(daemons: dict): + """Сохранить список демонов""" + with open(DAEMONS_FILE, 'w', encoding='utf-8') as f: + json.dump(daemons, f, indent=2, ensure_ascii=False) + + +async def check_daemon_connection(address: str, port: int, key: str) -> dict: + """Проверить подключение к демону""" + url = f"http://{address}:{port}/api/status" + headers = {"Authorization": f"Bearer {key}"} + + print(f"[DEBUG] Checking daemon connection:") + print(f"[DEBUG] URL: {url}") + print(f"[DEBUG] Key: {key[:20]}...") + + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(url, headers=headers) + print(f"[DEBUG] Status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f"[DEBUG] Response: {data}") + return data + else: + print(f"[DEBUG] Error response: {response.text}") + raise HTTPException(status_code=400, detail=f"Failed to connect to daemon: {response.status_code}") + except httpx.RequestError as e: + print(f"[DEBUG] Connection error: {e}") + raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}") + + +@router.get("/api/daemons") +async def get_daemons(current_user: dict = Depends(get_current_user_from_token)): + """Получить список всех демонов""" + # Только админы и владельцы могут видеть демоны + if current_user["role"] not in ["owner", "admin"]: + raise HTTPException(status_code=403, detail="Access denied") + + daemons = load_daemons() + + # Проверяем статус каждого демона + result = [] + for daemon_id, daemon in daemons.items(): + daemon_info = { + "id": daemon_id, + **daemon, + "status": "offline" + } + + try: + # Пытаемся получить статус + status = await check_daemon_connection( + daemon["address"], + daemon["port"], + daemon["key"] + ) + daemon_info["status"] = "online" + daemon_info["system"] = status.get("system", {}) + daemon_info["servers"] = status.get("servers", {}) + except: + pass + + result.append(daemon_info) + + return result + + +@router.post("/api/daemons") +async def create_daemon( + daemon: DaemonCreate, + current_user: dict = Depends(get_current_user_from_token) +): + """Добавить новый демон""" + if current_user["role"] not in ["owner", "admin"]: + raise HTTPException(status_code=403, detail="Access denied") + + # Проверяем подключение + await check_daemon_connection(daemon.address, daemon.port, daemon.key) + + daemons = load_daemons() + + # Генерируем ID + daemon_id = f"daemon-{len(daemons) + 1}" + + daemons[daemon_id] = { + "name": daemon.name, + "address": daemon.address, + "port": daemon.port, + "key": daemon.key, + "remarks": daemon.remarks, + "created_at": str(Path().cwd()) # Временная метка + } + + save_daemons(daemons) + + return { + "success": True, + "daemon_id": daemon_id, + "message": "Daemon added successfully" + } + + +@router.get("/api/daemons/{daemon_id}") +async def get_daemon( + daemon_id: str, + current_user: dict = Depends(get_current_user_from_token) +): + """Получить информацию о демоне""" + if current_user["role"] not in ["owner", "admin"]: + raise HTTPException(status_code=403, detail="Access denied") + + daemons = load_daemons() + + if daemon_id not in daemons: + raise HTTPException(status_code=404, detail="Daemon not found") + + daemon = daemons[daemon_id] + + # Получаем статус + try: + status = await check_daemon_connection( + daemon["address"], + daemon["port"], + daemon["key"] + ) + daemon["status"] = "online" + daemon["system"] = status.get("system", {}) + daemon["servers"] = status.get("servers", {}) + except: + daemon["status"] = "offline" + + return { + "id": daemon_id, + **daemon + } + + +@router.put("/api/daemons/{daemon_id}") +async def update_daemon( + daemon_id: str, + daemon_update: DaemonUpdate, + current_user: dict = Depends(get_current_user_from_token) +): + """Обновить демон""" + if current_user["role"] not in ["owner", "admin"]: + raise HTTPException(status_code=403, detail="Access denied") + + daemons = load_daemons() + + if daemon_id not in daemons: + raise HTTPException(status_code=404, detail="Daemon not found") + + # Обновляем поля + daemon = daemons[daemon_id] + + if daemon_update.name: + daemon["name"] = daemon_update.name + if daemon_update.address: + daemon["address"] = daemon_update.address + if daemon_update.port: + daemon["port"] = daemon_update.port + if daemon_update.key: + daemon["key"] = daemon_update.key + if daemon_update.remarks is not None: + daemon["remarks"] = daemon_update.remarks + + # Проверяем подключение с новыми данными + await check_daemon_connection( + daemon["address"], + daemon["port"], + daemon["key"] + ) + + save_daemons(daemons) + + return { + "success": True, + "message": "Daemon updated successfully" + } + + +@router.delete("/api/daemons/{daemon_id}") +async def delete_daemon( + daemon_id: str, + current_user: dict = Depends(get_current_user_from_token) +): + """Удалить демон""" + if current_user["role"] not in ["owner", "admin"]: + raise HTTPException(status_code=403, detail="Access denied") + + daemons = load_daemons() + + if daemon_id not in daemons: + raise HTTPException(status_code=404, detail="Daemon not found") + + del daemons[daemon_id] + save_daemons(daemons) + + return { + "success": True, + "message": "Daemon deleted successfully" + } + + +@router.get("/api/daemons/{daemon_id}/servers") +async def get_daemon_servers( + daemon_id: str, + current_user: dict = Depends(get_current_user_from_token) +): + """Получить список серверов на демоне""" + daemons = load_daemons() + + if daemon_id not in daemons: + raise HTTPException(status_code=404, detail="Daemon not found") + + daemon = daemons[daemon_id] + + url = f"http://{daemon['address']}:{daemon['port']}/api/servers" + headers = {"Authorization": f"Bearer {daemon['key']}"} + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get(url, headers=headers) + if response.status_code == 200: + return response.json() + else: + raise HTTPException(status_code=400, detail="Failed to get servers from daemon") + except httpx.RequestError as e: + raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}") diff --git a/backend/data/daemons.json b/backend/data/daemons.json new file mode 100644 index 0000000..82bd106 --- /dev/null +++ b/backend/data/daemons.json @@ -0,0 +1,10 @@ +{ + "daemon-1": { + "name": "Test", + "address": "127.0.0.1", + "port": 24444, + "key": "JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4", + "remarks": "", + "created_at": "D:\\Desktop\\adadad" + } +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index f57756e..13243ed 100644 --- a/backend/main.py +++ b/backend/main.py @@ -332,7 +332,7 @@ async def register(data: dict): save_users(users) - access_token = create_access_token(data={"sub": username}) + access_token = create_access_token(data={"sub": username, "role": role}) return { "access_token": access_token, "token_type": "bearer", @@ -353,7 +353,7 @@ async def login(data: dict): if not verify_password(password, user["password"]): raise HTTPException(401, "Неверное имя пользователя или пароль") - access_token = create_access_token(data={"sub": username}) + access_token = create_access_token(data={"sub": username, "role": user["role"]}) return { "access_token": access_token, "token_type": "bearer", @@ -838,12 +838,12 @@ async def get_servers(user: dict = Depends(get_current_user)): servers = [] try: # Владелец и администратор видят все серверы - can_view_all = user.get("role") in ["owner", "admin"] or user.get("permissions", {}).get("view_all_resources", False) + is_admin_or_owner = user.get("role") in ["owner", "admin"] for server_dir in SERVERS_DIR.iterdir(): if server_dir.is_dir(): # Проверка доступа: владелец/админ видят всё, остальные только свои - if not can_view_all and server_dir.name not in user.get("servers", []): + if not is_admin_or_owner and server_dir.name not in user.get("servers", []): continue config = load_server_config(server_dir.name) @@ -859,9 +859,14 @@ async def get_servers(user: dict = Depends(get_current_user)): servers.append({ "name": server_dir.name, "displayName": config.get("displayName", server_dir.name), - "status": "running" if is_running else "stopped" + "status": "running" if is_running else "stopped", + "owner": config.get("owner", "unknown") }) - print(f"Найдено серверов для {user['username']} ({user.get('role', 'user')}): {len(servers)}") + + print(f"[DEBUG] User: {user['username']} (role: {user.get('role', 'user')})") + print(f"[DEBUG] Is admin/owner: {is_admin_or_owner}") + print(f"[DEBUG] Servers found: {len(servers)}") + except Exception as e: print(f"Ошибка загрузки серверов: {e}") return servers @@ -869,34 +874,84 @@ async def get_servers(user: dict = Depends(get_current_user)): @app.post("/api/servers/create") async def create_server(data: dict, user: dict = Depends(get_current_user)): server_name = data.get("name", "").strip() + daemon_id = data.get("daemonId", "local") + if not server_name or not server_name.replace("_", "").replace("-", "").isalnum(): raise HTTPException(400, "Недопустимое имя сервера") - server_path = SERVERS_DIR / server_name - if server_path.exists(): - raise HTTPException(400, "Сервер с таким именем уже существует") + # Если создаем на демоне + if daemon_id != "local": + # Загружаем демоны + from daemons import load_daemons + daemons = load_daemons() + + if daemon_id not in daemons: + raise HTTPException(404, "Демон не найден") + + daemon = daemons[daemon_id] + + # Отправляем запрос на создание сервера на демоне + url = f"http://{daemon['address']}:{daemon['port']}/api/servers/create" + headers = {"Authorization": f"Bearer {daemon['key']}"} + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.post(url, json={ + "name": server_name, + "displayName": data.get("displayName", server_name), + "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), + "owner": user["username"] + }, headers=headers) + + if response.status_code != 200: + raise HTTPException(400, f"Ошибка создания сервера на демоне: {response.text}") + except httpx.RequestError as e: + raise HTTPException(400, f"Ошибка подключения к демону: {str(e)}") + + # Сохраняем информацию о сервере локально + config = { + "name": server_name, + "displayName": data.get("displayName", server_name), + "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), + "owner": user["username"], + "daemonId": daemon_id, + "daemonName": daemon["name"] + } + + # Создаем локальную запись о сервере + server_path = SERVERS_DIR / f"{daemon_id}_{server_name}" + server_path.mkdir(parents=True, exist_ok=True) + save_server_config(f"{daemon_id}_{server_name}", config) + + else: + # Создаем локально + server_path = SERVERS_DIR / server_name + if server_path.exists(): + raise HTTPException(400, "Сервер с таким именем уже существует") + + server_path.mkdir(parents=True) + + config = { + "name": server_name, + "displayName": data.get("displayName", server_name), + "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), + "owner": user["username"], + "daemonId": "local" + } + save_server_config(server_name, config) - server_path.mkdir(parents=True) - - config = { - "name": server_name, - "displayName": data.get("displayName", server_name), - "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), - "owner": user["username"] # Сохраняем владельца - } - save_server_config(server_name, config) - - # Если пользователь не админ, автоматически выдаем ему доступ - if user["role"] != "admin": + # Если пользователь не админ/owner, автоматически выдаем ему доступ + if user["role"] not in ["admin", "owner"]: users = load_users() if user["username"] in users: if "servers" not in users[user["username"]]: users[user["username"]]["servers"] = [] - if server_name not in users[user["username"]]["servers"]: - users[user["username"]]["servers"].append(server_name) + server_key = f"{daemon_id}_{server_name}" if daemon_id != "local" else server_name + if server_key not in users[user["username"]]["servers"]: + users[user["username"]]["servers"].append(server_key) save_users(users) - return {"message": "Сервер создан", "name": server_name} + return {"message": "Сервер создан", "name": server_name, "daemonId": daemon_id} @app.get("/api/servers/{server_name}/config") async def get_server_config(server_name: str, user: dict = Depends(get_current_user)): @@ -1869,6 +1924,16 @@ async def update_user_permissions(username: str, perms: PermissionsUpdate, curre return {"message": f"Права пользователя {username} обновлены", "permissions": perms.permissions} +# ============================================ +# API для управления демонами +# ============================================ + +from daemons import router as daemons_router + +# Подключаем роутер демонов +app.include_router(daemons_router) + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/users.json b/backend/users.json index 9e26dfe..8e34366 100644 --- a/backend/users.json +++ b/backend/users.json @@ -1 +1,22 @@ -{} \ No newline at end of file +{ + "admin": { + "username": "admin", + "password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG", + "role": "owner", + "servers": [], + "permissions": { + "manage_users": true, + "manage_roles": true, + "manage_servers": true, + "manage_tickets": true, + "manage_files": true, + "delete_users": true, + "view_all_resources": true + }, + "resource_access": { + "servers": [], + "tickets": [], + "files": [] + } + } +} \ No newline at end of file diff --git a/daemon/.env b/daemon/.env new file mode 100644 index 0000000..0f909b6 --- /dev/null +++ b/daemon/.env @@ -0,0 +1,11 @@ +# Daemon Configuration +DAEMON_ID=daemon-1 +DAEMON_NAME=Main +DAEMON_PORT=24444 +DAEMON_KEY=JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4 + +# Panel Connection (optional, for WebSocket) +PANEL_URL=ws://0.0.0.0:8000 + +# Servers Directory +SERVERS_DIR=./servers diff --git a/daemon/.env.example b/daemon/.env.example new file mode 100644 index 0000000..b9442d7 --- /dev/null +++ b/daemon/.env.example @@ -0,0 +1,20 @@ +# MC Panel Daemon Configuration + +# Уникальный ID демона +DAEMON_ID=daemon-1 + +# Название демона (отображается в панели) +DAEMON_NAME=Main Server + +# Порт, на котором будет работать демон +DAEMON_PORT=24444 + +# Секретный ключ для аутентификации (должен совпадать с ключом в панели) +# Сгенерируйте случайный ключ: python -c "import secrets; print(secrets.token_urlsafe(32))" +DAEMON_KEY=your-secret-key-here + +# URL основной панели (для WebSocket подключения, опционально) +PANEL_URL=http://your-panel-url:8000 + +# Директория для серверов +SERVERS_DIR=./servers diff --git a/daemon/README.md b/daemon/README.md new file mode 100644 index 0000000..9adddb0 --- /dev/null +++ b/daemon/README.md @@ -0,0 +1,195 @@ +# MC Panel Daemon + +Удаленный демон для управления серверами Minecraft. Устанавливается на отдельные физические серверы и подключается к основной панели управления. + +## Установка + +### Windows + +1. Установите Python 3.8 или выше +2. Скопируйте папку `daemon` на удаленный сервер +3. Откройте командную строку в папке daemon +4. Запустите установку зависимостей: + ``` + install.bat + ``` + +### Linux + +1. Установите Python 3.8 или выше +2. Скопируйте папку `daemon` на удаленный сервер +3. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +## Настройка + +1. Скопируйте `.env.example` в `.env`: + ``` + copy .env.example .env + ``` + +2. Отредактируйте `.env` файл: + ```env + DAEMON_ID=daemon-1 + DAEMON_NAME=Main Server + DAEMON_PORT=24444 + DAEMON_KEY=your-secret-key-here + SERVERS_DIR=./servers + ``` + + - `DAEMON_ID` - уникальный ID демона + - `DAEMON_NAME` - отображаемое имя демона + - `DAEMON_PORT` - порт для API (по умолчанию 24444) + - `DAEMON_KEY` - секретный ключ для аутентификации + - `SERVERS_DIR` - директория для серверов + +3. Создайте секретный ключ: + ```python + import secrets + print(secrets.token_urlsafe(32)) + ``` + +## Запуск + +### Windows +``` +start.bat +``` + +### Linux +```bash +python main.py +``` + +### Как сервис (Linux) + +Создайте файл `/etc/systemd/system/mcpanel-daemon.service`: + +```ini +[Unit] +Description=MC Panel Daemon +After=network.target + +[Service] +Type=simple +User=mcpanel +WorkingDirectory=/path/to/daemon +ExecStart=/usr/bin/python3 /path/to/daemon/main.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Запустите сервис: +```bash +sudo systemctl enable mcpanel-daemon +sudo systemctl start mcpanel-daemon +sudo systemctl status mcpanel-daemon +``` + +## Подключение к панели + +1. Откройте основную панель управления +2. Перейдите в раздел "Демоны" +3. Нажмите "Добавить демон" +4. Заполните данные: + - **Название**: Main Server + - **IP адрес**: IP адрес сервера с демоном + - **Порт**: 24444 (или ваш порт) + - **Ключ демона**: ваш DAEMON_KEY из .env +5. Нажмите "Добавить" + +## API Endpoints + +Демон предоставляет следующие API endpoints: + +- `GET /` - Информация о демоне +- `GET /api/status` - Статус демона и системы +- `GET /api/servers` - Список серверов +- `POST /api/servers/{name}/start` - Запустить сервер +- `POST /api/servers/{name}/stop` - Остановить сервер +- `POST /api/servers/{name}/command` - Отправить команду +- `GET /api/servers/{name}/stats` - Статистика сервера + +## Безопасность + +1. **Используйте сильный ключ** - генерируйте случайный ключ длиной минимум 32 символа +2. **Настройте файрвол** - разрешите доступ к порту демона только с IP основной панели +3. **Используйте HTTPS** - в продакшене используйте reverse proxy (nginx) с SSL +4. **Регулярно обновляйте** - следите за обновлениями и устанавливайте их + +## Пример конфигурации nginx (с SSL) + +```nginx +server { + listen 443 ssl http2; + server_name daemon.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:24444; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## Структура директорий + +``` +daemon/ +├── main.py # Основной файл демона +├── requirements.txt # Зависимости Python +├── .env # Конфигурация (создайте из .env.example) +├── .env.example # Пример конфигурации +├── install.bat # Скрипт установки (Windows) +├── start.bat # Скрипт запуска (Windows) +├── README.md # Эта документация +└── servers/ # Директория с серверами + ├── server1/ + │ ├── config.json + │ └── ... + └── server2/ + ├── config.json + └── ... +``` + +## Формат config.json для сервера + +```json +{ + "name": "server1", + "displayName": "My Minecraft Server", + "startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui" +} +``` + +## Troubleshooting + +### Демон не запускается +- Проверьте, что Python установлен: `python --version` +- Проверьте, что все зависимости установлены: `pip list` +- Проверьте логи в консоли + +### Панель не может подключиться к демону +- Проверьте, что демон запущен +- Проверьте файрвол и порты +- Проверьте, что ключ в панели совпадает с DAEMON_KEY +- Проверьте IP адрес и порт + +### Сервер не запускается +- Проверьте startCommand в config.json +- Проверьте права доступа к файлам +- Проверьте логи сервера + +## Поддержка + +Если у вас возникли проблемы, создайте тикет в системе поддержки панели. diff --git a/daemon/install.bat b/daemon/install.bat new file mode 100644 index 0000000..41f8908 --- /dev/null +++ b/daemon/install.bat @@ -0,0 +1,9 @@ +@echo off +echo Installing MC Panel Daemon dependencies... +pip install -r requirements.txt +echo. +echo Installation complete! +echo. +echo Please configure .env file before starting the daemon +echo Copy .env.example to .env and edit it +pause diff --git a/daemon/main.py b/daemon/main.py new file mode 100644 index 0000000..8441f34 --- /dev/null +++ b/daemon/main.py @@ -0,0 +1,307 @@ +""" +MC Panel Daemon - Удаленный демон для управления серверами +Устанавливается на отдельные серверы и подключается к основной панели +""" + +import asyncio +import json +import os +import psutil +import subprocess +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional +import websockets +from fastapi import FastAPI, HTTPException, Header +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +from dotenv import load_dotenv + +load_dotenv() + +app = FastAPI(title="MC Panel Daemon") + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Конфигурация +DAEMON_ID = os.getenv("DAEMON_ID", "daemon-1") +DAEMON_NAME = os.getenv("DAEMON_NAME", "Main Server") +DAEMON_PORT = int(os.getenv("DAEMON_PORT", "24444")) +DAEMON_KEY = os.getenv("DAEMON_KEY", "") # Ключ для аутентификации +PANEL_URL = os.getenv("PANEL_URL", "") # URL основной панели для WebSocket +SERVERS_DIR = Path(os.getenv("SERVERS_DIR", "./servers")) + +# Создаем директорию для серверов +SERVERS_DIR.mkdir(exist_ok=True) + +# Хранилище процессов серверов +server_processes: Dict[str, subprocess.Popen] = {} + + +def verify_key(authorization: str = Header(None)) -> bool: + """Проверка API ключа""" + if not DAEMON_KEY: + return True # Если ключ не установлен, разрешаем доступ + + if not authorization: + raise HTTPException(status_code=401, detail="Missing authorization header") + + if authorization != f"Bearer {DAEMON_KEY}": + raise HTTPException(status_code=403, detail="Invalid daemon key") + + return True + + +@app.get("/") +async def root(): + """Информация о демоне""" + return { + "daemon_id": DAEMON_ID, + "daemon_name": DAEMON_NAME, + "status": "online", + "version": "1.0.0" + } + + +@app.get("/api/status") +async def get_status(authorization: str = Header(None)): + """Получить статус демона и системы""" + # Проверка ключа опциональна для статуса + if DAEMON_KEY and authorization: + if authorization != f"Bearer {DAEMON_KEY}": + raise HTTPException(status_code=403, detail="Invalid daemon key") + + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + return { + "daemon_id": DAEMON_ID, + "daemon_name": DAEMON_NAME, + "status": "online", + "system": { + "cpu_usage": cpu_percent, + "memory_total": memory.total, + "memory_used": memory.used, + "memory_percent": memory.percent, + "disk_total": disk.total, + "disk_used": disk.used, + "disk_percent": disk.percent + }, + "servers": { + "total": len(list(SERVERS_DIR.iterdir())), + "running": len(server_processes) + }, + "timestamp": datetime.now().isoformat() + } + + +@app.get("/api/servers") +async def list_servers(authorization: str = Header(None)): + """Список всех серверов на этом демоне""" + verify_key(authorization) + + servers = [] + for server_dir in SERVERS_DIR.iterdir(): + if server_dir.is_dir(): + config_file = server_dir / "config.json" + if config_file.exists(): + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + servers.append({ + "name": server_dir.name, + "display_name": config.get("displayName", server_dir.name), + "status": "running" if server_dir.name in server_processes else "stopped", + "daemon_id": DAEMON_ID + }) + + return servers + + +@app.post("/api/servers/{server_name}/start") +async def start_server(server_name: str, authorization: str = Header(None)): + """Запустить сервер""" + verify_key(authorization) + + server_dir = SERVERS_DIR / server_name + if not server_dir.exists(): + raise HTTPException(status_code=404, detail="Server not found") + + if server_name in server_processes: + raise HTTPException(status_code=400, detail="Server already running") + + config_file = server_dir / "config.json" + if not config_file.exists(): + raise HTTPException(status_code=400, detail="Server config not found") + + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + start_command = config.get("startCommand", "") + if not start_command: + raise HTTPException(status_code=400, detail="Start command not configured") + + try: + # Запускаем процесс + process = subprocess.Popen( + start_command, + shell=True, + cwd=str(server_dir), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE + ) + + server_processes[server_name] = process + + return { + "success": True, + "message": f"Server {server_name} started", + "pid": process.pid + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to start server: {str(e)}") + + +@app.post("/api/servers/{server_name}/stop") +async def stop_server(server_name: str, authorization: str = Header(None)): + """Остановить сервер""" + verify_key(authorization) + + if server_name not in server_processes: + raise HTTPException(status_code=400, detail="Server not running") + + try: + process = server_processes[server_name] + process.terminate() + + # Ждем завершения процесса + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + + del server_processes[server_name] + + return { + "success": True, + "message": f"Server {server_name} stopped" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to stop server: {str(e)}") + + +@app.post("/api/servers/{server_name}/command") +async def send_command(server_name: str, command: dict, authorization: str = Header(None)): + """Отправить команду в консоль сервера""" + verify_key(authorization) + + if server_name not in server_processes: + raise HTTPException(status_code=400, detail="Server not running") + + try: + process = server_processes[server_name] + cmd = command.get("command", "") + + if process.stdin: + process.stdin.write(f"{cmd}\n".encode()) + process.stdin.flush() + + return { + "success": True, + "message": "Command sent" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to send command: {str(e)}") + + +@app.get("/api/servers/{server_name}/stats") +async def get_server_stats(server_name: str, authorization: str = Header(None)): + """Получить статистику сервера""" + verify_key(authorization) + + server_dir = SERVERS_DIR / server_name + if not server_dir.exists(): + raise HTTPException(status_code=404, detail="Server not found") + + is_running = server_name in server_processes + cpu = 0 + memory = 0 + + if is_running: + try: + process = server_processes[server_name] + p = psutil.Process(process.pid) + cpu = p.cpu_percent(interval=0.1) + memory = p.memory_info().rss / 1024 / 1024 # MB + except: + pass + + # Размер директории + disk_usage = sum(f.stat().st_size for f in server_dir.rglob('*') if f.is_file()) / 1024 / 1024 # MB + + return { + "status": "running" if is_running else "stopped", + "cpu": cpu, + "memory": memory, + "disk": disk_usage + } + + +@app.post("/api/servers/create") +async def create_server_on_daemon(data: dict, authorization: str = Header(None)): + """Создать сервер на этом демоне""" + verify_key(authorization) + + server_name = data.get("name", "").strip() + if not server_name: + raise HTTPException(status_code=400, detail="Server name is required") + + server_path = SERVERS_DIR / server_name + + if server_path.exists(): + raise HTTPException(status_code=400, detail="Server already exists") + + try: + server_path.mkdir(parents=True) + + # Сохраняем конфигурацию + config = { + "name": server_name, + "displayName": data.get("displayName", server_name), + "startCommand": data.get("startCommand", "java -Xmx2G -jar server.jar nogui"), + "owner": data.get("owner", "unknown") + } + + config_file = server_path / "config.json" + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + return {"message": "Server created successfully", "name": server_name} + except Exception as e: + # Если что-то пошло не так, удаляем папку + if server_path.exists(): + import shutil + shutil.rmtree(server_path) + raise HTTPException(status_code=500, detail=f"Failed to create server: {str(e)}") + + +if __name__ == "__main__": + print(f"Starting MC Panel Daemon: {DAEMON_NAME} ({DAEMON_ID})") + print(f"Port: {DAEMON_PORT}") + print(f"Servers directory: {SERVERS_DIR}") + + uvicorn.run( + app, + host="0.0.0.0", + port=DAEMON_PORT, + log_level="info" + ) diff --git a/daemon/requirements.txt b/daemon/requirements.txt new file mode 100644 index 0000000..17b2c82 --- /dev/null +++ b/daemon/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +websockets==12.0 +psutil==5.9.6 +python-dotenv==1.0.0 diff --git a/daemon/start.bat b/daemon/start.bat new file mode 100644 index 0000000..cd305dc --- /dev/null +++ b/daemon/start.bat @@ -0,0 +1,4 @@ +@echo off +echo Starting MC Panel Daemon... +python main.py +pause diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 40179b8..0dbb544 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import Users from './components/Users'; import UserManagement from './components/UserManagement'; import Tickets from './components/Tickets'; import Profile from './components/Profile'; +import Daemons from './components/Daemons'; import Auth from './components/Auth'; import ErrorBoundary from './components/ErrorBoundary'; import NotificationSystem, { notify } from './components/NotificationSystem'; @@ -27,6 +28,7 @@ function App() { const [activeTab, setActiveTab] = useState('console'); const [showCreateModal, setShowCreateModal] = useState(false); const [showUserManagement, setShowUserManagement] = useState(false); + const [showDaemons, setShowDaemons] = useState(false); const [showTickets, setShowTickets] = useState(false); const [showProfile, setShowProfile] = useState(false); const [connectionError, setConnectionError] = useState(false); @@ -216,17 +218,30 @@ function App() { {/* Bottom section */}
- {user?.role === 'owner' && ( - + {(user?.role === 'owner' || user?.role === 'admin') && ( + <> + + + + )}
)} + {showDaemons && ( +
setShowDaemons(false)}> +
e.stopPropagation()}> + +
+
+ )} + {showTickets && (
setShowTickets(false)}>
e.stopPropagation()}> diff --git a/frontend/src/components/CreateServerModal.jsx b/frontend/src/components/CreateServerModal.jsx index b2648c2..632d150 100644 --- a/frontend/src/components/CreateServerModal.jsx +++ b/frontend/src/components/CreateServerModal.jsx @@ -1,16 +1,36 @@ -import { useState } from 'react'; -import { X } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { X, Server } from 'lucide-react'; import axios from 'axios'; import { API_URL } from '../config'; import { notify } from './NotificationSystem'; -export default function CreateServerModal({ token, theme, onClose, onCreated }) { +export default function CreateServerModal({ token, onClose, onSuccess }) { const [formData, setFormData] = useState({ name: '', displayName: '', - startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui' + startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui', + daemonId: 'local' // По умолчанию локальный }); const [loading, setLoading] = useState(false); + const [daemons, setDaemons] = useState([]); + const [loadingDaemons, setLoadingDaemons] = useState(true); + + useEffect(() => { + loadDaemons(); + }, []); + + const loadDaemons = async () => { + try { + const { data } = await axios.get(`${API_URL}/api/daemons`, { + headers: { Authorization: `Bearer ${token}` } + }); + setDaemons(data.filter(d => d.status === 'online')); // Только онлайн демоны + } catch (error) { + console.error('Ошибка загрузки демонов:', error); + } finally { + setLoadingDaemons(false); + } + }; const handleSubmit = async (e) => { e.preventDefault(); @@ -23,11 +43,10 @@ export default function CreateServerModal({ token, theme, onClose, onCreated }) { headers: { Authorization: `Bearer ${token}` } } ); notify('success', 'Сервер создан', `Сервер "${formData.displayName}" успешно создан`); - onCreated(); + if (onSuccess) onSuccess(); onClose(); } catch (error) { notify('error', 'Ошибка создания', error.response?.data?.detail || 'Не удалось создать сервер'); - alert(error.response?.data?.detail || 'Ошибка создания сервера'); } finally { setLoading(false); } @@ -35,12 +54,12 @@ export default function CreateServerModal({ token, theme, onClose, onCreated }) return (
-
+
-

Создать сервер

+

Создать сервер

@@ -48,7 +67,34 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
-
+ +
+ setFormData({ ...formData, name: e.target.value })} - className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} + className="input" placeholder="my_server" />
-
-
@@ -92,14 +138,14 @@ export default function CreateServerModal({ token, theme, onClose, onCreated }) diff --git a/frontend/src/components/Daemons.jsx b/frontend/src/components/Daemons.jsx new file mode 100644 index 0000000..3f323ba --- /dev/null +++ b/frontend/src/components/Daemons.jsx @@ -0,0 +1,382 @@ +import { useState, useEffect } from 'react'; +import { Server, Plus, Trash2, Edit, RefreshCw, CheckCircle, XCircle, Activity } from 'lucide-react'; +import axios from 'axios'; +import { API_URL } from '../config'; +import { notify } from './NotificationSystem'; + +export default function Daemons({ token }) { + const [daemons, setDaemons] = useState([]); + const [loading, setLoading] = useState(true); + const [showAddModal, setShowAddModal] = useState(false); + const [editingDaemon, setEditingDaemon] = useState(null); + + const [formData, setFormData] = useState({ + name: '', + address: '', + port: 24444, + key: '', + remarks: '' + }); + + useEffect(() => { + loadDaemons(); + const interval = setInterval(loadDaemons, 10000); // Обновляем каждые 10 секунд + return () => clearInterval(interval); + }, []); + + const loadDaemons = async () => { + try { + const { data } = await axios.get(`${API_URL}/api/daemons`, { + headers: { Authorization: `Bearer ${token}` } + }); + setDaemons(data); + setLoading(false); + } catch (error) { + console.error('Ошибка загрузки демонов:', error); + setLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + if (editingDaemon) { + await axios.put( + `${API_URL}/api/daemons/${editingDaemon.id}`, + formData, + { headers: { Authorization: `Bearer ${token}` } } + ); + notify('success', 'Демон обновлен', 'Демон успешно обновлен'); + } else { + await axios.post( + `${API_URL}/api/daemons`, + formData, + { headers: { Authorization: `Bearer ${token}` } } + ); + notify('success', 'Демон добавлен', 'Демон успешно добавлен'); + } + + setShowAddModal(false); + setEditingDaemon(null); + setFormData({ name: '', address: '', port: 24444, key: '', remarks: '' }); + loadDaemons(); + } catch (error) { + notify('error', 'Ошибка', error.response?.data?.detail || 'Не удалось сохранить демон'); + } + }; + + const handleDelete = async (daemonId) => { + if (!confirm('Вы уверены, что хотите удалить этот демон?')) return; + + try { + await axios.delete(`${API_URL}/api/daemons/${daemonId}`, { + headers: { Authorization: `Bearer ${token}` } + }); + notify('success', 'Демон удален', 'Демон успешно удален'); + loadDaemons(); + } catch (error) { + notify('error', 'Ошибка удаления', error.response?.data?.detail || 'Не удалось удалить демон'); + } + }; + + const handleEdit = (daemon) => { + setEditingDaemon(daemon); + setFormData({ + name: daemon.name, + address: daemon.address, + port: daemon.port, + key: daemon.key, + remarks: daemon.remarks || '' + }); + setShowAddModal(true); + }; + + const formatBytes = (bytes) => { + if (!bytes) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + }; + + if (loading) { + return ( +
+
Загрузка демонов...
+
+ ); + } + + return ( +
+
+
+ +
+

Демоны

+

Управление удаленными серверами

+
+
+
+ + +
+
+ + {daemons.length === 0 ? ( +
+ +

Нет демонов

+

+ Добавьте первый демон для управления удаленными серверами +

+ +
+ ) : ( +
+ {daemons.map((daemon) => ( +
+
+
+
+ +
+
+
+

{daemon.name}

+ {daemon.status === 'online' ? ( + + + Онлайн + + ) : ( + + + Оффлайн + + )} +
+

+ {daemon.address}:{daemon.port} +

+ {daemon.remarks && ( +

{daemon.remarks}

+ )} +
+
+ +
+ + +
+
+ + {daemon.status === 'online' && daemon.system && ( +
+
+
+ + CPU +
+
{daemon.system.cpu_usage?.toFixed(1)}%
+
+
+
+
+ +
+
+ + ОЗУ +
+
{daemon.system.memory_percent?.toFixed(1)}%
+
+ {formatBytes(daemon.system.memory_used)} / {formatBytes(daemon.system.memory_total)} +
+
+
+
+
+ +
+
+ + Диск +
+
{daemon.system.disk_percent?.toFixed(1)}%
+
+ {formatBytes(daemon.system.disk_used)} / {formatBytes(daemon.system.disk_total)} +
+
+
+
+
+
+ )} + + {daemon.status === 'online' && daemon.servers && ( +
+
+ + Серверов: {daemon.servers.total || 0} + + Запущено: {daemon.servers.running || 0} +
+
+ )} +
+ ))} +
+ )} + + {/* Модальное окно добавления/редактирования */} + {showAddModal && ( +
setShowAddModal(false)}> +
e.stopPropagation()}> +

+ {editingDaemon ? 'Редактировать демон' : 'Добавить демон'} +

+ + +
+ + setFormData({ ...formData, name: e.target.value })} + className="input" + placeholder="Main Server" + /> +
+ +
+
+ + setFormData({ ...formData, address: e.target.value })} + className="input" + placeholder="192.168.1.100" + /> +
+ +
+ + setFormData({ ...formData, port: parseInt(e.target.value) })} + className="input" + placeholder="24444" + /> +
+
+ +
+ + setFormData({ ...formData, key: e.target.value })} + className="input" + placeholder="your-secret-key" + /> +

+ Ключ должен совпадать с DAEMON_KEY в .env файле демона +

+
+ +
+ +