mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-21 12:01:56 +00:00
feat: add Mieru management
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- Stopped putting web-admin password into systemd environment; admin now reads root-only auth file.
|
- Stopped putting web-admin password into systemd environment; admin now reads root-only auth file.
|
||||||
- Added public site manager for port 80 with install, remove, and custom HTML upload.
|
- Added public site manager for port 80 with install, remove, and custom HTML upload.
|
||||||
- Added README docs for custom domain and public site flow.
|
- Added README docs for custom domain and public site flow.
|
||||||
|
- Added Mieru / mita management in web-admin with install, port validation, client JSON, mihomo YAML, logs, services, and backup support.
|
||||||
|
|
||||||
## 2.5.0
|
## 2.5.0
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -71,6 +71,8 @@ ssh root@SERVER 'chmod +x /opt/pcatelegram_web/install.sh /opt/pcatelegram_web/i
|
|||||||
| `PCATELEGRAM_WEB_ADMIN_USER` | `admin` | Basic Auth login |
|
| `PCATELEGRAM_WEB_ADMIN_USER` | `admin` | Basic Auth login |
|
||||||
| `PCATELEGRAM_WEB_ADMIN_PASSWORD` | `admin` | web-admin password |
|
| `PCATELEGRAM_WEB_ADMIN_PASSWORD` | `admin` | web-admin password |
|
||||||
| `PCATELEGRAM_WEB_WARP_CONFIG` | `/opt/pcatelegram_web/warp.json` | WARP / WARP+ settings |
|
| `PCATELEGRAM_WEB_WARP_CONFIG` | `/opt/pcatelegram_web/warp.json` | WARP / WARP+ settings |
|
||||||
|
| `PCATELEGRAM_WEB_MIERU_CONFIG` | `/opt/pcatelegram_web/mieru.json` | Mieru server settings |
|
||||||
|
| `PCATELEGRAM_WEB_MIERU_SERVER_CONFIG` | `/opt/pcatelegram_web/mieru_server_config.json` | JSON applied by `mita apply config` |
|
||||||
| `PCATELEGRAM_WEB_SITE_ROOT` | `/var/www/pcatelegram_web-site` | root публичного HTML-сайта на 80 |
|
| `PCATELEGRAM_WEB_SITE_ROOT` | `/var/www/pcatelegram_web-site` | root публичного HTML-сайта на 80 |
|
||||||
| `PCATELEGRAM_WEB_NGINX_MASK_CONF` | `/etc/nginx/sites-available/pcatelegram_web-mask` | nginx config сайта на 80 |
|
| `PCATELEGRAM_WEB_NGINX_MASK_CONF` | `/etc/nginx/sites-available/pcatelegram_web-mask` | nginx config сайта на 80 |
|
||||||
|
|
||||||
@@ -124,6 +126,19 @@ WARP+ key не отдается в API целиком: web показывает
|
|||||||
|
|
||||||
Per-client runtime routing в текущем telemt не включается автоматически: публичные параметры telemt дают users/limits/quotas/ad tags, но не документируют привязку upstream к конкретному user. Для настоящего WARP только одному клиенту нужен отдельный telemt route/service или upstream-схема.
|
Per-client runtime routing в текущем telemt не включается автоматически: публичные параметры telemt дают users/limits/quotas/ad tags, но не документируют привязку upstream к конкретному user. Для настоящего WARP только одному клиенту нужен отдельный telemt route/service или upstream-схема.
|
||||||
|
|
||||||
|
## Mieru
|
||||||
|
|
||||||
|
В web-admin Settings есть блок `Mieru`:
|
||||||
|
|
||||||
|
- `Mieru port` — отдельный публичный порт `mita`, по умолчанию `2999`. Диапазон Mieru: `1025-65535`.
|
||||||
|
- `Transport` — `TCP` или `UDP`.
|
||||||
|
- `User` / `Password` — учётка Mieru. Если пароль пустой, web-admin генерирует новый.
|
||||||
|
- `Install / save Mieru` — если `mita` ещё нет, web-admin скачивает свежий пакет `mita` из GitHub release `enfein/mieru`, ставит его, применяет JSON через `mita apply config`, затем запускает `mita start`.
|
||||||
|
- `Client JSON` — готовый конфиг для официального `mieru` client.
|
||||||
|
- `mihomo YAML` — proxy block для клиентов с поддержкой `type: mieru`.
|
||||||
|
|
||||||
|
Mieru не меняет `telemt`, nginx, WARP и сайт на 80. Перед сохранением web-admin проверяет выбранный порт через `ss`; чужой listener блокирует сохранение. Файлы `mieru.json` и `mieru_server_config.json` хранятся с правами `0600` и входят в backup.
|
||||||
|
|
||||||
## Порт и маскировка
|
## Порт и маскировка
|
||||||
|
|
||||||
В web-admin Settings есть блок `Port and mask site`:
|
В web-admin Settings есть блок `Port and mask site`:
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ SHARED_443_CONFIG = Path(os.getenv("PCATELEGRAM_WEB_SHARED_443", "/opt/pcatelegr
|
|||||||
BACKUP_SCHEDULE_FILE = Path(os.getenv("PCATELEGRAM_WEB_BACKUP_SCHEDULE", "/opt/pcatelegram_web/backup_schedule.json"))
|
BACKUP_SCHEDULE_FILE = Path(os.getenv("PCATELEGRAM_WEB_BACKUP_SCHEDULE", "/opt/pcatelegram_web/backup_schedule.json"))
|
||||||
BACKUP_RESTORE_LOG = Path(os.getenv("PCATELEGRAM_WEB_BACKUP_RESTORE_LOG", "/var/log/pcatelegram_web-restore.log"))
|
BACKUP_RESTORE_LOG = Path(os.getenv("PCATELEGRAM_WEB_BACKUP_RESTORE_LOG", "/var/log/pcatelegram_web-restore.log"))
|
||||||
WARP_CONFIG_FILE = Path(os.getenv("PCATELEGRAM_WEB_WARP_CONFIG", "/opt/pcatelegram_web/warp.json"))
|
WARP_CONFIG_FILE = Path(os.getenv("PCATELEGRAM_WEB_WARP_CONFIG", "/opt/pcatelegram_web/warp.json"))
|
||||||
|
MIERU_CONFIG_FILE = Path(os.getenv("PCATELEGRAM_WEB_MIERU_CONFIG", "/opt/pcatelegram_web/mieru.json"))
|
||||||
|
MIERU_SERVER_CONFIG_FILE = Path(os.getenv("PCATELEGRAM_WEB_MIERU_SERVER_CONFIG", "/opt/pcatelegram_web/mieru_server_config.json"))
|
||||||
WEBSITE_ROOT = Path(os.getenv("PCATELEGRAM_WEB_SITE_ROOT", "/var/www/pcatelegram_web-site"))
|
WEBSITE_ROOT = Path(os.getenv("PCATELEGRAM_WEB_SITE_ROOT", "/var/www/pcatelegram_web-site"))
|
||||||
NGINX_MASK_CONF = Path(os.getenv("PCATELEGRAM_WEB_NGINX_MASK_CONF", "/etc/nginx/sites-available/pcatelegram_web-mask"))
|
NGINX_MASK_CONF = Path(os.getenv("PCATELEGRAM_WEB_NGINX_MASK_CONF", "/etc/nginx/sites-available/pcatelegram_web-mask"))
|
||||||
NGINX_MASK_LINK = Path(os.getenv("PCATELEGRAM_WEB_NGINX_MASK_LINK", "/etc/nginx/sites-enabled/pcatelegram_web-mask"))
|
NGINX_MASK_LINK = Path(os.getenv("PCATELEGRAM_WEB_NGINX_MASK_LINK", "/etc/nginx/sites-enabled/pcatelegram_web-mask"))
|
||||||
@@ -751,6 +753,328 @@ def apply_warp_runtime(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_mieru_port(value: Any) -> int:
|
||||||
|
port = normalize_port(value)
|
||||||
|
if port < 1025:
|
||||||
|
raise ValueError("Mieru port must be between 1025 and 65535")
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_mieru_protocol(value: Any) -> str:
|
||||||
|
proto = str(value or "TCP").strip().upper()
|
||||||
|
if proto not in {"TCP", "UDP"}:
|
||||||
|
raise ValueError("Mieru protocol must be TCP or UDP")
|
||||||
|
return proto
|
||||||
|
|
||||||
|
|
||||||
|
def read_mieru_config() -> dict[str, Any]:
|
||||||
|
raw = load_json(MIERU_CONFIG_FILE, {}) or {}
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raw = {}
|
||||||
|
user = str(raw.get("user") or "main").strip()
|
||||||
|
if not USER_RE.match(user):
|
||||||
|
user = "main"
|
||||||
|
try:
|
||||||
|
port = normalize_mieru_port(raw.get("port") or 2999)
|
||||||
|
except ValueError:
|
||||||
|
port = 2999
|
||||||
|
try:
|
||||||
|
protocol = normalize_mieru_protocol(raw.get("protocol") or "TCP")
|
||||||
|
except ValueError:
|
||||||
|
protocol = "TCP"
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"enabled": bool(raw.get("enabled")),
|
||||||
|
"port": port,
|
||||||
|
"protocol": protocol,
|
||||||
|
"user": user,
|
||||||
|
"password": str(raw.get("password") or "").strip(),
|
||||||
|
"updated_at": str(raw.get("updated_at") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_mieru_config(config: dict[str, Any]) -> None:
|
||||||
|
cfg = dict(config)
|
||||||
|
cfg["version"] = 1
|
||||||
|
cfg["updated_at"] = utc_now()
|
||||||
|
save_json(MIERU_CONFIG_FILE, cfg, mode=0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_installed() -> bool:
|
||||||
|
return bool(shutil.which("mita")) or service_status("mita") != "not_installed"
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_status_text() -> str:
|
||||||
|
mita = shutil.which("mita")
|
||||||
|
if not mita:
|
||||||
|
return "mita not installed"
|
||||||
|
code, out, err = run([mita, "status"], timeout=8)
|
||||||
|
text = (out or err).strip()
|
||||||
|
return text or f"mita status exit {code}"
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_port_conflicts(port: int, protocol: str) -> list[dict[str, Any]]:
|
||||||
|
listeners, _ = collect_port_listeners(port)
|
||||||
|
proto = protocol.upper()
|
||||||
|
conflicts = []
|
||||||
|
for item in listeners:
|
||||||
|
if str(item.get("proto") or "").upper() != proto:
|
||||||
|
continue
|
||||||
|
process = str(item.get("process") or "").lower()
|
||||||
|
if "mita" in process or "mieru" in process:
|
||||||
|
continue
|
||||||
|
conflicts.append(item)
|
||||||
|
return conflicts
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_server_config(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"portBindings": [
|
||||||
|
{
|
||||||
|
"port": int(cfg["port"]),
|
||||||
|
"protocol": cfg["protocol"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": cfg["user"],
|
||||||
|
"password": cfg["password"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"loggingLevel": "INFO",
|
||||||
|
"mtu": 1400,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_client_config(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
host = public_host_for_notes()
|
||||||
|
ip_address = host if re.match(r"^[0-9a-fA-F:.]+$", host) else ""
|
||||||
|
domain_name = "" if ip_address else host
|
||||||
|
return {
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profileName": "PCAtelegram_web",
|
||||||
|
"user": {
|
||||||
|
"name": cfg["user"],
|
||||||
|
"password": cfg["password"],
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"ipAddress": ip_address,
|
||||||
|
"domainName": domain_name,
|
||||||
|
"portBindings": [
|
||||||
|
{
|
||||||
|
"port": int(cfg["port"]),
|
||||||
|
"protocol": cfg["protocol"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mtu": 1400,
|
||||||
|
"multiplexing": {
|
||||||
|
"level": "MULTIPLEXING_LOW",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeProfile": "PCAtelegram_web",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_mihomo_proxy(cfg: dict[str, Any]) -> str:
|
||||||
|
host = public_host_for_notes()
|
||||||
|
return "\n".join([
|
||||||
|
"proxies:",
|
||||||
|
" - name: PCAtelegram_web Mieru",
|
||||||
|
" type: mieru",
|
||||||
|
f" server: {host}",
|
||||||
|
f" port: {int(cfg['port'])}",
|
||||||
|
f" transport: {cfg['protocol']}",
|
||||||
|
f" username: {cfg['user']}",
|
||||||
|
f" password: {cfg['password']}",
|
||||||
|
" multiplexing: MULTIPLEXING_LOW",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def public_mieru_config() -> dict[str, Any]:
|
||||||
|
cfg = read_mieru_config()
|
||||||
|
listeners, errors = collect_port_listeners(cfg["port"])
|
||||||
|
conflicts = mieru_port_conflicts(cfg["port"], cfg["protocol"])
|
||||||
|
status_text = mieru_status_text()
|
||||||
|
installed = mieru_installed()
|
||||||
|
running = installed and ("RUNNING" in status_text.upper() or any(
|
||||||
|
str(item.get("proto") or "").upper() == cfg["protocol"]
|
||||||
|
and ("mita" in str(item.get("process") or "").lower() or "mieru" in str(item.get("process") or "").lower())
|
||||||
|
for item in listeners
|
||||||
|
))
|
||||||
|
return {
|
||||||
|
"enabled": cfg["enabled"],
|
||||||
|
"installed": installed,
|
||||||
|
"running": running,
|
||||||
|
"service": service_status("mita"),
|
||||||
|
"status_text": status_text,
|
||||||
|
"port": cfg["port"],
|
||||||
|
"protocol": cfg["protocol"],
|
||||||
|
"user": cfg["user"],
|
||||||
|
"password": cfg["password"],
|
||||||
|
"password_mask": mask_secret(cfg["password"]),
|
||||||
|
"updated_at": cfg["updated_at"],
|
||||||
|
"listeners": listeners,
|
||||||
|
"conflicts": conflicts,
|
||||||
|
"ok": not errors,
|
||||||
|
"error": "; ".join(errors[:2]),
|
||||||
|
"client_config": mieru_client_config(cfg) if cfg["password"] else {},
|
||||||
|
"mihomo_yaml": mieru_mihomo_proxy(cfg) if cfg["password"] else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def latest_mita_asset_url() -> tuple[str, str]:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://api.github.com/repos/enfein/mieru/releases/latest",
|
||||||
|
headers={"User-Agent": "PCAtelegram_web-admin"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||||
|
release = json.loads(resp.read(1024 * 1024).decode("utf-8"))
|
||||||
|
assets = release.get("assets") if isinstance(release, dict) else []
|
||||||
|
if not isinstance(assets, list):
|
||||||
|
raise RuntimeError("invalid Mieru release metadata")
|
||||||
|
machine = os.uname().machine.lower()
|
||||||
|
arch_aliases = ["amd64", "x86_64"] if machine in {"x86_64", "amd64"} else ["arm64", "aarch64"] if machine in {"aarch64", "arm64"} else [machine]
|
||||||
|
if shutil.which("apt-get") or shutil.which("dpkg"):
|
||||||
|
extensions = [".deb"]
|
||||||
|
elif shutil.which("dnf") or shutil.which("yum") or shutil.which("rpm"):
|
||||||
|
extensions = [".rpm"]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("supported package manager not found")
|
||||||
|
for asset in assets:
|
||||||
|
name = str(asset.get("name") or "").lower()
|
||||||
|
url = str(asset.get("browser_download_url") or "")
|
||||||
|
if not url or ".sha256" in name:
|
||||||
|
continue
|
||||||
|
if not any(name.endswith(ext) for ext in extensions):
|
||||||
|
continue
|
||||||
|
if "mita" not in name:
|
||||||
|
continue
|
||||||
|
if any(alias in name for alias in arch_aliases):
|
||||||
|
return url, name
|
||||||
|
raise RuntimeError(f"mita package for {machine} not found in latest Mieru release")
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, target: Path) -> None:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "PCAtelegram_web-admin"})
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
target.write_bytes(resp.read())
|
||||||
|
|
||||||
|
|
||||||
|
def install_mita_package() -> dict[str, Any]:
|
||||||
|
result: dict[str, Any] = {"attempted": False, "installed": mieru_installed(), "asset": "", "warnings": []}
|
||||||
|
if result["installed"] and shutil.which("mita"):
|
||||||
|
return result
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
result["warnings"].append("root required to install mita")
|
||||||
|
return result
|
||||||
|
url, name = latest_mita_asset_url()
|
||||||
|
target = Path("/tmp") / name
|
||||||
|
download_file(url, target)
|
||||||
|
result.update({"attempted": True, "asset": name})
|
||||||
|
if name.endswith(".deb"):
|
||||||
|
run(["apt-get", "update"], timeout=180)
|
||||||
|
code, _, err = run(["dpkg", "-i", str(target)], timeout=120)
|
||||||
|
if code != 0:
|
||||||
|
run(["apt-get", "install", "-f", "-y"], timeout=240)
|
||||||
|
elif name.endswith(".rpm"):
|
||||||
|
rpm = shutil.which("rpm")
|
||||||
|
if not rpm:
|
||||||
|
raise RuntimeError("rpm not found")
|
||||||
|
code, _, err = run([rpm, "-Uvh", "--force", str(target)], timeout=180)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(err.strip() or "rpm install failed")
|
||||||
|
run(["systemctl", "enable", "--now", "mita"], timeout=30)
|
||||||
|
result["installed"] = bool(shutil.which("mita")) or service_status("mita") != "not_installed"
|
||||||
|
if not result["installed"]:
|
||||||
|
result["warnings"].append("mita still not available after install")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def apply_mieru_config(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
server_cfg = mieru_server_config(cfg)
|
||||||
|
MIERU_SERVER_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = MIERU_SERVER_CONFIG_FILE.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(server_cfg, ensure_ascii=False, indent=4) + "\n", encoding="utf-8")
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
tmp.replace(MIERU_SERVER_CONFIG_FILE)
|
||||||
|
mita = shutil.which("mita")
|
||||||
|
if not mita:
|
||||||
|
raise RuntimeError("mita is not installed")
|
||||||
|
code, out, err = run([mita, "apply", "config", str(MIERU_SERVER_CONFIG_FILE)], timeout=30)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(err.strip() or out.strip() or "mita apply config failed")
|
||||||
|
run([mita, "stop"], timeout=20)
|
||||||
|
code, out, err = run([mita, "start"], timeout=30)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(err.strip() or out.strip() or "mita start failed")
|
||||||
|
return {"applied": True, "status": mieru_status_text()}
|
||||||
|
|
||||||
|
|
||||||
|
def save_mieru_settings(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
current = read_mieru_config()
|
||||||
|
port = normalize_mieru_port(body.get("port") or current["port"])
|
||||||
|
protocol = normalize_mieru_protocol(body.get("protocol") or current["protocol"])
|
||||||
|
user = str(body.get("user") or current["user"] or "main").strip()
|
||||||
|
if not USER_RE.match(user):
|
||||||
|
raise ValueError("invalid Mieru user")
|
||||||
|
password = str(body.get("password") or "").strip() or current.get("password") or secrets.token_urlsafe(18)
|
||||||
|
conflicts = mieru_port_conflicts(port, protocol)
|
||||||
|
if conflicts:
|
||||||
|
names = ", ".join(f"{item.get('process')} {item.get('address')}" for item in conflicts[:3])
|
||||||
|
raise RuntimeError(f"Mieru port is busy: {names}")
|
||||||
|
install_result = install_mita_package()
|
||||||
|
if not install_result.get("installed"):
|
||||||
|
raise RuntimeError("; ".join(install_result.get("warnings") or ["mita install failed"]))
|
||||||
|
cfg = {
|
||||||
|
"enabled": True,
|
||||||
|
"port": port,
|
||||||
|
"protocol": protocol,
|
||||||
|
"user": user,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
apply_result = apply_mieru_config(cfg)
|
||||||
|
write_mieru_config(cfg)
|
||||||
|
payload = public_mieru_config()
|
||||||
|
payload["install"] = install_result
|
||||||
|
payload["apply"] = apply_result
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def control_mieru(action: str) -> dict[str, Any]:
|
||||||
|
if not mieru_installed():
|
||||||
|
raise RuntimeError("mita is not installed")
|
||||||
|
mita = shutil.which("mita")
|
||||||
|
if not mita:
|
||||||
|
raise RuntimeError("mita command not found")
|
||||||
|
cfg = read_mieru_config()
|
||||||
|
if action == "stop":
|
||||||
|
code, out, err = run([mita, "stop"], timeout=20)
|
||||||
|
cfg["enabled"] = False
|
||||||
|
write_mieru_config(cfg)
|
||||||
|
elif action == "start":
|
||||||
|
if not cfg.get("password"):
|
||||||
|
raise RuntimeError("Mieru config is empty")
|
||||||
|
code, out, err = run([mita, "start"], timeout=30)
|
||||||
|
cfg["enabled"] = code == 0
|
||||||
|
write_mieru_config(cfg)
|
||||||
|
elif action == "restart":
|
||||||
|
if cfg.get("password"):
|
||||||
|
apply_mieru_config(cfg)
|
||||||
|
return public_mieru_config()
|
||||||
|
code, out, err = run(["systemctl", "restart", "mita"], timeout=30)
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported Mieru action")
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(err.strip() or out.strip() or f"mita {action} failed")
|
||||||
|
return public_mieru_config()
|
||||||
|
|
||||||
|
|
||||||
def read_disabled_users() -> dict[str, str]:
|
def read_disabled_users() -> dict[str, str]:
|
||||||
raw = load_json(DISABLED_USERS_FILE, {}) or {}
|
raw = load_json(DISABLED_USERS_FILE, {}) or {}
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
@@ -982,6 +1306,8 @@ def _process_role(process: str) -> str:
|
|||||||
lowered = process.lower()
|
lowered = process.lower()
|
||||||
if "telemt" in lowered or "mtproto" in lowered:
|
if "telemt" in lowered or "mtproto" in lowered:
|
||||||
return "mtproxy"
|
return "mtproxy"
|
||||||
|
if "mita" in lowered or "mieru" in lowered:
|
||||||
|
return "mieru"
|
||||||
if "nginx" in lowered or "apache" in lowered or "caddy" in lowered:
|
if "nginx" in lowered or "apache" in lowered or "caddy" in lowered:
|
||||||
return "site"
|
return "site"
|
||||||
if "xray" in lowered or "x-ui" in lowered or "3x-ui" in lowered or "xui" in lowered:
|
if "xray" in lowered or "x-ui" in lowered or "3x-ui" in lowered or "xui" in lowered:
|
||||||
@@ -2082,7 +2408,7 @@ def user_qr_png(name: str) -> tuple[bytes, str]:
|
|||||||
|
|
||||||
|
|
||||||
def read_log_payload(service: str) -> dict[str, Any]:
|
def read_log_payload(service: str) -> dict[str, Any]:
|
||||||
allowed = {"telemt", "nginx", "pcatelegram_web-bot", "pcatelegram_web-stats", "pcatelegram_web-admin"}
|
allowed = {"telemt", "nginx", "mita", "pcatelegram_web-bot", "pcatelegram_web-stats", "pcatelegram_web-admin"}
|
||||||
if service not in allowed:
|
if service not in allowed:
|
||||||
raise ValueError("unsupported service")
|
raise ValueError("unsupported service")
|
||||||
code, stdout, stderr = run(["journalctl", "-u", service, "-n", "180", "--no-pager", "-o", "short-iso"], timeout=10)
|
code, stdout, stderr = run(["journalctl", "-u", service, "-n", "180", "--no-pager", "-o", "short-iso"], timeout=10)
|
||||||
@@ -2139,6 +2465,7 @@ def overview_payload() -> dict[str, Any]:
|
|||||||
summary = telemt_api("/v1/stats/summary")
|
summary = telemt_api("/v1/stats/summary")
|
||||||
services = {
|
services = {
|
||||||
"telemt": service_status("telemt"),
|
"telemt": service_status("telemt"),
|
||||||
|
"mieru": service_status("mita"),
|
||||||
"nginx": service_status("nginx"),
|
"nginx": service_status("nginx"),
|
||||||
"bot": service_status("pcatelegram_web-bot"),
|
"bot": service_status("pcatelegram_web-bot"),
|
||||||
"stats": service_status("pcatelegram_web-stats"),
|
"stats": service_status("pcatelegram_web-stats"),
|
||||||
@@ -2163,6 +2490,7 @@ def overview_payload() -> dict[str, Any]:
|
|||||||
"backups": list_backups(),
|
"backups": list_backups(),
|
||||||
"backup_schedule": backup_schedule_status(),
|
"backup_schedule": backup_schedule_status(),
|
||||||
"warp": public_warp_config(),
|
"warp": public_warp_config(),
|
||||||
|
"mieru": public_mieru_config(),
|
||||||
"routing": routing_payload(),
|
"routing": routing_payload(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2326,6 +2654,8 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_json({"ok": True, "data": routing_payload(port)})
|
self.send_json({"ok": True, "data": routing_payload(port)})
|
||||||
elif path == "/api/warp":
|
elif path == "/api/warp":
|
||||||
self.send_json({"ok": True, "data": public_warp_config()})
|
self.send_json({"ok": True, "data": public_warp_config()})
|
||||||
|
elif path == "/api/mieru":
|
||||||
|
self.send_json({"ok": True, "data": public_mieru_config()})
|
||||||
elif path == "/api/users":
|
elif path == "/api/users":
|
||||||
users = read_user_records()
|
users = read_user_records()
|
||||||
latest = latest_user_stats()
|
latest = latest_user_stats()
|
||||||
@@ -2678,11 +3008,28 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_error_json(500, f"failed to save WARP config: {exc}")
|
self.send_error_json(500, f"failed to save WARP config: {exc}")
|
||||||
return
|
return
|
||||||
self.send_json({"ok": True, "data": {"config": public_warp_config(), "apply": apply_result}})
|
self.send_json({"ok": True, "data": {"config": public_warp_config(), "apply": apply_result}})
|
||||||
|
elif path == "/api/mieru":
|
||||||
|
action = str(body.get("action") or "install").strip().lower()
|
||||||
|
try:
|
||||||
|
if action in {"install", "save"}:
|
||||||
|
payload = save_mieru_settings(body)
|
||||||
|
else:
|
||||||
|
payload = control_mieru(action)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.send_error_json(400, str(exc))
|
||||||
|
return
|
||||||
|
except RuntimeError as exc:
|
||||||
|
self.send_error_json(409, str(exc))
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
self.send_error_json(500, f"failed to manage Mieru: {exc}")
|
||||||
|
return
|
||||||
|
self.send_json({"ok": True, "data": payload})
|
||||||
elif path == "/api/auth/logout":
|
elif path == "/api/auth/logout":
|
||||||
self.handle_logout()
|
self.handle_logout()
|
||||||
elif path.startswith("/api/services/") and path.endswith("/restart"):
|
elif path.startswith("/api/services/") and path.endswith("/restart"):
|
||||||
service = path[len("/api/services/"):-len("/restart")]
|
service = path[len("/api/services/"):-len("/restart")]
|
||||||
allowed = {"telemt", "nginx", "pcatelegram_web-bot", "pcatelegram_web-stats"}
|
allowed = {"telemt", "nginx", "mita", "pcatelegram_web-bot", "pcatelegram_web-stats"}
|
||||||
if service not in allowed:
|
if service not in allowed:
|
||||||
self.send_error_json(400, "unsupported service")
|
self.send_error_json(400, "unsupported service")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -132,6 +132,24 @@ const i18n = {
|
|||||||
warpNotInstalled: "will install on save",
|
warpNotInstalled: "will install on save",
|
||||||
warpPerUserNote: "One-client WARP profile is stored here. If warp-cli is missing, it will be installed on save. Runtime per-client routing needs a dedicated telemt route; global WARP applies to all clients.",
|
warpPerUserNote: "One-client WARP profile is stored here. If warp-cli is missing, it will be installed on save. Runtime per-client routing needs a dedicated telemt route; global WARP applies to all clients.",
|
||||||
warpAllNote: "Global WARP applies to all proxy traffic. If warp-cli is missing, it will be installed on save, then connected.",
|
warpAllNote: "Global WARP applies to all proxy traffic. If warp-cli is missing, it will be installed on save, then connected.",
|
||||||
|
mieruEyebrow: "Protocol",
|
||||||
|
mieruTitle: "Mieru",
|
||||||
|
mieruPort: "Mieru port",
|
||||||
|
mieruProtocol: "Transport",
|
||||||
|
mieruUser: "User",
|
||||||
|
mieruPassword: "Password",
|
||||||
|
mieruInstall: "Install / save Mieru",
|
||||||
|
mieruStart: "Start",
|
||||||
|
mieruStop: "Stop",
|
||||||
|
mieruInstalled: "Mieru installed",
|
||||||
|
mieruNotInstalled: "will install on save",
|
||||||
|
mieruRunning: "Mieru running",
|
||||||
|
mieruStopped: "Mieru stopped",
|
||||||
|
mieruSaved: "Mieru settings saved",
|
||||||
|
mieruClientConfig: "Client JSON",
|
||||||
|
mieruMihomoYaml: "mihomo YAML",
|
||||||
|
mieruNote: "Mieru server uses mita. Port must be free and in range 1025-65535. Client JSON is for mieru app; YAML is for clients with mieru support.",
|
||||||
|
copyConfig: "Copy config",
|
||||||
savedKey: "saved key",
|
savedKey: "saved key",
|
||||||
siteMaskEyebrow: "Public site",
|
siteMaskEyebrow: "Public site",
|
||||||
siteMaskTitle: "Domain and mask site",
|
siteMaskTitle: "Domain and mask site",
|
||||||
@@ -239,6 +257,7 @@ const i18n = {
|
|||||||
roleSite: "Website",
|
roleSite: "Website",
|
||||||
roleXray: "Xray / 3x-ui",
|
roleXray: "Xray / 3x-ui",
|
||||||
roleAmneziawg: "AmneziaWG",
|
roleAmneziawg: "AmneziaWG",
|
||||||
|
roleMieru: "Mieru",
|
||||||
roleOther: "Other",
|
roleOther: "Other",
|
||||||
range15m: "15 min",
|
range15m: "15 min",
|
||||||
range1h: "1 hour",
|
range1h: "1 hour",
|
||||||
@@ -402,6 +421,24 @@ const i18n = {
|
|||||||
warpNotInstalled: "установится при сохранении",
|
warpNotInstalled: "установится при сохранении",
|
||||||
warpPerUserNote: "Профиль WARP для одного клиента сохраняется здесь. Если warp-cli нет, он установится при сохранении. Runtime-маршрут на одного клиента требует отдельный маршрут telemt; global WARP действует на всех клиентов.",
|
warpPerUserNote: "Профиль WARP для одного клиента сохраняется здесь. Если warp-cli нет, он установится при сохранении. Runtime-маршрут на одного клиента требует отдельный маршрут telemt; global WARP действует на всех клиентов.",
|
||||||
warpAllNote: "Global WARP применится ко всему proxy-трафику. Если warp-cli нет, он установится при сохранении и затем подключится.",
|
warpAllNote: "Global WARP применится ко всему proxy-трафику. Если warp-cli нет, он установится при сохранении и затем подключится.",
|
||||||
|
mieruEyebrow: "Протокол",
|
||||||
|
mieruTitle: "Mieru",
|
||||||
|
mieruPort: "Порт Mieru",
|
||||||
|
mieruProtocol: "Транспорт",
|
||||||
|
mieruUser: "Пользователь",
|
||||||
|
mieruPassword: "Пароль",
|
||||||
|
mieruInstall: "Установить / сохранить Mieru",
|
||||||
|
mieruStart: "Запустить",
|
||||||
|
mieruStop: "Остановить",
|
||||||
|
mieruInstalled: "Mieru установлен",
|
||||||
|
mieruNotInstalled: "установится при сохранении",
|
||||||
|
mieruRunning: "Mieru работает",
|
||||||
|
mieruStopped: "Mieru остановлен",
|
||||||
|
mieruSaved: "Настройки Mieru сохранены",
|
||||||
|
mieruClientConfig: "Client JSON",
|
||||||
|
mieruMihomoYaml: "mihomo YAML",
|
||||||
|
mieruNote: "Mieru server работает через mita. Порт должен быть свободен и в диапазоне 1025-65535. Client JSON для mieru app; YAML для клиентов с поддержкой mieru.",
|
||||||
|
copyConfig: "Копировать конфиг",
|
||||||
savedKey: "ключ сохранён",
|
savedKey: "ключ сохранён",
|
||||||
siteMaskEyebrow: "Публичный сайт",
|
siteMaskEyebrow: "Публичный сайт",
|
||||||
siteMaskTitle: "Домен и сайт маскировки",
|
siteMaskTitle: "Домен и сайт маскировки",
|
||||||
@@ -509,6 +546,7 @@ const i18n = {
|
|||||||
roleSite: "Сайт",
|
roleSite: "Сайт",
|
||||||
roleXray: "Xray / 3x-ui",
|
roleXray: "Xray / 3x-ui",
|
||||||
roleAmneziawg: "AmneziaWG",
|
roleAmneziawg: "AmneziaWG",
|
||||||
|
roleMieru: "Mieru",
|
||||||
roleOther: "Другое",
|
roleOther: "Другое",
|
||||||
range15m: "15 мин",
|
range15m: "15 мин",
|
||||||
range1h: "1 час",
|
range1h: "1 час",
|
||||||
@@ -565,6 +603,7 @@ const state = {
|
|||||||
routingDirty: false,
|
routingDirty: false,
|
||||||
routingPreviewMap: null,
|
routingPreviewMap: null,
|
||||||
warp: null,
|
warp: null,
|
||||||
|
mieru: null,
|
||||||
siteMask: null,
|
siteMask: null,
|
||||||
qrLink: "",
|
qrLink: "",
|
||||||
pendingUsers: new Set(),
|
pendingUsers: new Set(),
|
||||||
@@ -833,6 +872,7 @@ function healthLabel(health) {
|
|||||||
function renderServices(services = {}) {
|
function renderServices(services = {}) {
|
||||||
const items = [
|
const items = [
|
||||||
{ key: "telemt", label: "telemt", api: "telemt" },
|
{ key: "telemt", label: "telemt", api: "telemt" },
|
||||||
|
{ key: "mieru", label: "mieru", api: "mita" },
|
||||||
{ key: "nginx", label: "nginx", api: "nginx" },
|
{ key: "nginx", label: "nginx", api: "nginx" },
|
||||||
{ key: "bot", label: "bot", api: "pcatelegram_web-bot" },
|
{ key: "bot", label: "bot", api: "pcatelegram_web-bot" },
|
||||||
{ key: "stats", label: "stats", api: "pcatelegram_web-stats" },
|
{ key: "stats", label: "stats", api: "pcatelegram_web-stats" },
|
||||||
@@ -1422,6 +1462,52 @@ function renderWarpSettings() {
|
|||||||
: `${t("warpAllNote")}${statusLine ? ` ${statusLine}` : ""}`;
|
: `${t("warpAllNote")}${statusLine ? ` ${statusLine}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mieruSummary(cfg = {}) {
|
||||||
|
const conflicts = cfg.conflicts || [];
|
||||||
|
if (conflicts.length) {
|
||||||
|
return `${t("routingBusy")}: ${conflicts.map((item) => `${item.process} ${item.address}`).join(", ")}`;
|
||||||
|
}
|
||||||
|
const listeners = cfg.listeners || [];
|
||||||
|
const listenerText = listeners.length
|
||||||
|
? listeners.map((item) => `${item.process} ${item.address}`).join(", ")
|
||||||
|
: `${cfg.protocol || "TCP"} :${cfg.port || 2999}`;
|
||||||
|
const status = cfg.status_text ? ` ${cfg.status_text}` : "";
|
||||||
|
return `${listenerText}.${status} ${t("mieruNote")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMieruSettings(payload = null) {
|
||||||
|
const cfg = payload || state.mieru || state.overview?.mieru || {};
|
||||||
|
const portEl = $("#mieruPort");
|
||||||
|
const protocolEl = $("#mieruProtocol");
|
||||||
|
const userEl = $("#mieruUser");
|
||||||
|
const passwordEl = $("#mieruPassword");
|
||||||
|
const statusEl = $("#mieruStatus");
|
||||||
|
const noteEl = $("#mieruRuntimeNote");
|
||||||
|
const clientEl = $("#mieruClientConfig");
|
||||||
|
const yamlEl = $("#mieruMihomoYaml");
|
||||||
|
if (!portEl || !protocolEl || !userEl || !passwordEl || !statusEl || !noteEl) return;
|
||||||
|
if (document.activeElement !== portEl) portEl.value = cfg.port || 2999;
|
||||||
|
if (document.activeElement !== protocolEl) protocolEl.value = cfg.protocol || "TCP";
|
||||||
|
if (document.activeElement !== userEl) userEl.value = cfg.user || "main";
|
||||||
|
if (document.activeElement !== passwordEl) passwordEl.value = "";
|
||||||
|
passwordEl.placeholder = cfg.password_mask ? `${t("savedKey")}: ${cfg.password_mask}` : "auto";
|
||||||
|
const conflicts = cfg.conflicts || [];
|
||||||
|
statusEl.textContent = conflicts.length
|
||||||
|
? t("routingBusy")
|
||||||
|
: (cfg.running ? t("mieruRunning") : (cfg.installed ? t("mieruStopped") : t("mieruNotInstalled")));
|
||||||
|
statusEl.className = `status-pill ${conflicts.length ? "health-error" : (cfg.running ? "health-ok" : "")}`;
|
||||||
|
noteEl.textContent = mieruSummary(cfg);
|
||||||
|
noteEl.classList.toggle("error", Boolean(conflicts.length));
|
||||||
|
if (clientEl && document.activeElement !== clientEl) {
|
||||||
|
clientEl.value = cfg.client_config && Object.keys(cfg.client_config).length
|
||||||
|
? JSON.stringify(cfg.client_config, null, 2)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
if (yamlEl && document.activeElement !== yamlEl) {
|
||||||
|
yamlEl.value = cfg.mihomo_yaml || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function siteMaskSummary(payload = {}) {
|
function siteMaskSummary(payload = {}) {
|
||||||
const conflicts = payload.conflicts || [];
|
const conflicts = payload.conflicts || [];
|
||||||
if (conflicts.length) {
|
if (conflicts.length) {
|
||||||
@@ -1612,6 +1698,7 @@ async function refreshAll(options = {}) {
|
|||||||
state.backupSchedule = state.overview.backup_schedule || state.backupSchedule;
|
state.backupSchedule = state.overview.backup_schedule || state.backupSchedule;
|
||||||
state.routing = state.overview.routing || state.routing;
|
state.routing = state.overview.routing || state.routing;
|
||||||
state.warp = state.overview.warp || state.warp;
|
state.warp = state.overview.warp || state.warp;
|
||||||
|
state.mieru = state.overview.mieru || state.mieru;
|
||||||
state.siteMask = state.overview.site_mask || state.siteMask;
|
state.siteMask = state.overview.site_mask || state.siteMask;
|
||||||
updateLanguageFromOverview(state.overview);
|
updateLanguageFromOverview(state.overview);
|
||||||
state.users = await api("/api/users");
|
state.users = await api("/api/users");
|
||||||
@@ -1638,6 +1725,7 @@ async function refreshAll(options = {}) {
|
|||||||
renderUsers();
|
renderUsers();
|
||||||
renderRoutingSettings();
|
renderRoutingSettings();
|
||||||
renderWarpSettings();
|
renderWarpSettings();
|
||||||
|
renderMieruSettings();
|
||||||
renderSiteMaskSettings();
|
renderSiteMaskSettings();
|
||||||
if (state.page === "traffic") {
|
if (state.page === "traffic") {
|
||||||
await refreshStats();
|
await refreshStats();
|
||||||
@@ -1884,6 +1972,56 @@ async function saveWarpSettings(eventObj) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMieruSettings(eventObj) {
|
||||||
|
eventObj.preventDefault();
|
||||||
|
const form = eventObj.currentTarget;
|
||||||
|
const controls = Array.from(form.querySelectorAll("input, select, button"));
|
||||||
|
controls.forEach((control) => { control.disabled = true; });
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
action: "install",
|
||||||
|
port: Number.parseInt($("#mieruPort").value, 10),
|
||||||
|
protocol: $("#mieruProtocol").value,
|
||||||
|
user: $("#mieruUser").value.trim(),
|
||||||
|
password: $("#mieruPassword").value.trim(),
|
||||||
|
};
|
||||||
|
const data = await api("/api/mieru", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
state.mieru = data;
|
||||||
|
$("#mieruPassword").value = "";
|
||||||
|
renderMieruSettings(data);
|
||||||
|
addEvent(t("mieruSaved"), `${data.protocol}:${data.port}`);
|
||||||
|
toast(t("mieruSaved"));
|
||||||
|
await refreshAll();
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message);
|
||||||
|
} finally {
|
||||||
|
controls.forEach((control) => { control.disabled = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function controlMieru(action) {
|
||||||
|
const buttons = [$("#mieruStartBtn"), $("#mieruRestartBtn"), $("#mieruStopBtn")].filter(Boolean);
|
||||||
|
buttons.forEach((button) => { button.disabled = true; });
|
||||||
|
try {
|
||||||
|
const data = await api("/api/mieru", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
});
|
||||||
|
state.mieru = data;
|
||||||
|
renderMieruSettings(data);
|
||||||
|
addEvent(t("serviceRestarted"), `mieru ${action}`);
|
||||||
|
toast(action === "stop" ? t("mieruStopped") : t("mieruRunning"));
|
||||||
|
await refreshAll();
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message);
|
||||||
|
} finally {
|
||||||
|
buttons.forEach((button) => { button.disabled = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSiteMask(eventObj) {
|
async function saveSiteMask(eventObj) {
|
||||||
eventObj.preventDefault();
|
eventObj.preventDefault();
|
||||||
const form = eventObj.currentTarget;
|
const form = eventObj.currentTarget;
|
||||||
@@ -2211,6 +2349,11 @@ $("#routingPort").addEventListener("change", (eventObj) => checkRoutingPort(even
|
|||||||
$("#warpSettingsForm").addEventListener("submit", saveWarpSettings);
|
$("#warpSettingsForm").addEventListener("submit", saveWarpSettings);
|
||||||
$("#warpScope").addEventListener("change", renderWarpSettings);
|
$("#warpScope").addEventListener("change", renderWarpSettings);
|
||||||
$("#warpUserSelect").addEventListener("change", renderWarpSettings);
|
$("#warpUserSelect").addEventListener("change", renderWarpSettings);
|
||||||
|
$("#mieruSettingsForm").addEventListener("submit", saveMieruSettings);
|
||||||
|
$("#mieruStartBtn").addEventListener("click", () => controlMieru("start"));
|
||||||
|
$("#mieruRestartBtn").addEventListener("click", () => controlMieru("restart"));
|
||||||
|
$("#mieruStopBtn").addEventListener("click", () => controlMieru("stop"));
|
||||||
|
$("#mieruCopyBtn").addEventListener("click", () => copyText($("#mieruClientConfig").value || $("#mieruMihomoYaml").value || ""));
|
||||||
$("#siteMaskForm").addEventListener("submit", saveSiteMask);
|
$("#siteMaskForm").addEventListener("submit", saveSiteMask);
|
||||||
$("#siteMaskRemoveBtn").addEventListener("click", removeSiteMask);
|
$("#siteMaskRemoveBtn").addEventListener("click", removeSiteMask);
|
||||||
$("#siteMaskUploadBtn").addEventListener("click", uploadSiteMaskHtml);
|
$("#siteMaskUploadBtn").addEventListener("click", uploadSiteMaskHtml);
|
||||||
|
|||||||
@@ -374,6 +374,7 @@
|
|||||||
<div class="inline-form">
|
<div class="inline-form">
|
||||||
<select id="logService">
|
<select id="logService">
|
||||||
<option value="telemt">telemt</option>
|
<option value="telemt">telemt</option>
|
||||||
|
<option value="mita">mieru / mita</option>
|
||||||
<option value="nginx">nginx</option>
|
<option value="nginx">nginx</option>
|
||||||
<option value="pcatelegram_web-bot">bot</option>
|
<option value="pcatelegram_web-bot">bot</option>
|
||||||
<option value="pcatelegram_web-stats">stats</option>
|
<option value="pcatelegram_web-stats">stats</option>
|
||||||
@@ -465,6 +466,53 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="mieruEyebrow">Protocol</p>
|
||||||
|
<h2 data-i18n="mieruTitle">Mieru</h2>
|
||||||
|
</div>
|
||||||
|
<span id="mieruStatus" class="status-pill">--</span>
|
||||||
|
</div>
|
||||||
|
<form id="mieruSettingsForm" class="settings-form">
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruPort">Mieru port</span>
|
||||||
|
<input name="port" id="mieruPort" type="number" min="1025" max="65535" step="1" inputmode="numeric" placeholder="2999">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruProtocol">Transport</span>
|
||||||
|
<select name="protocol" id="mieruProtocol">
|
||||||
|
<option value="TCP">TCP</option>
|
||||||
|
<option value="UDP">UDP</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruUser">User</span>
|
||||||
|
<input name="user" id="mieruUser" autocomplete="off" spellcheck="false" placeholder="main">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruPassword">Password</span>
|
||||||
|
<input name="password" id="mieruPassword" autocomplete="off" spellcheck="false" placeholder="auto">
|
||||||
|
</label>
|
||||||
|
<p id="mieruRuntimeNote" class="modal-note"></p>
|
||||||
|
<div class="inline-form">
|
||||||
|
<button type="submit" data-i18n="mieruInstall">Install / save Mieru</button>
|
||||||
|
<button id="mieruStartBtn" class="soft" type="button" data-i18n="mieruStart">Start</button>
|
||||||
|
<button id="mieruRestartBtn" class="soft" type="button" data-i18n="restart">Restart</button>
|
||||||
|
<button id="mieruStopBtn" class="danger" type="button" data-i18n="mieruStop">Stop</button>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruClientConfig">Client JSON</span>
|
||||||
|
<textarea id="mieruClientConfig" rows="8" readonly spellcheck="false"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruMihomoYaml">mihomo YAML</span>
|
||||||
|
<textarea id="mieruMihomoYaml" rows="8" readonly spellcheck="false"></textarea>
|
||||||
|
</label>
|
||||||
|
<button id="mieruCopyBtn" class="soft" type="button" data-i18n="copyConfig">Copy config</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
@@ -508,6 +556,6 @@
|
|||||||
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/app.js?v=2.5.0-admin29" type="module"></script>
|
<script src="/app.js?v=2.5.0-admin30" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ body {
|
|||||||
font: 14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font: 14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, select { font: inherit; }
|
button, input, select, textarea { font: inherit; }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
@@ -74,7 +74,7 @@ button.soft { background: color-mix(in srgb, var(--blue) 12%, transparent); colo
|
|||||||
button.danger { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }
|
button.danger { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }
|
||||||
button.attention { background: var(--amber); color: #111827; }
|
button.attention { background: var(--amber); color: #111827; }
|
||||||
|
|
||||||
input, select {
|
input, select, textarea {
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -85,7 +85,13 @@ input, select {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus, select:focus {
|
textarea {
|
||||||
|
padding-block: 10px;
|
||||||
|
resize: vertical;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
border-color: var(--blue);
|
border-color: var(--blue);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 16%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 16%, transparent);
|
||||||
}
|
}
|
||||||
@@ -1360,6 +1366,14 @@ td small {
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
.backup-item span,
|
.backup-item span,
|
||||||
.event small {
|
.event small {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ create_backup() {
|
|||||||
if [ -f "$PCATELEGRAM_WEB_DIR/warp.json" ]; then
|
if [ -f "$PCATELEGRAM_WEB_DIR/warp.json" ]; then
|
||||||
cp "$PCATELEGRAM_WEB_DIR/warp.json" "$tmp_dir/warp.json" 2>/dev/null
|
cp "$PCATELEGRAM_WEB_DIR/warp.json" "$tmp_dir/warp.json" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$PCATELEGRAM_WEB_DIR/mieru.json" ]; then
|
||||||
|
cp "$PCATELEGRAM_WEB_DIR/mieru.json" "$tmp_dir/mieru.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$PCATELEGRAM_WEB_DIR/mieru_server_config.json" ]; then
|
||||||
|
cp "$PCATELEGRAM_WEB_DIR/mieru_server_config.json" "$tmp_dir/mieru_server_config.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Language marker (i18n)
|
# Language marker (i18n)
|
||||||
if [ -f "$PCATELEGRAM_WEB_DIR/.language" ]; then
|
if [ -f "$PCATELEGRAM_WEB_DIR/.language" ]; then
|
||||||
@@ -253,6 +259,12 @@ restore_backup() {
|
|||||||
if [ ! -f "$backup_dir/warp.json" ] && [ -f "$tmp_dir/opt/pcatelegram_web/warp.json" ]; then
|
if [ ! -f "$backup_dir/warp.json" ] && [ -f "$tmp_dir/opt/pcatelegram_web/warp.json" ]; then
|
||||||
cp "$tmp_dir/opt/pcatelegram_web/warp.json" "$backup_dir/warp.json" 2>/dev/null || true
|
cp "$tmp_dir/opt/pcatelegram_web/warp.json" "$backup_dir/warp.json" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
if [ ! -f "$backup_dir/mieru.json" ] && [ -f "$tmp_dir/opt/pcatelegram_web/mieru.json" ]; then
|
||||||
|
cp "$tmp_dir/opt/pcatelegram_web/mieru.json" "$backup_dir/mieru.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ ! -f "$backup_dir/mieru_server_config.json" ] && [ -f "$tmp_dir/opt/pcatelegram_web/mieru_server_config.json" ]; then
|
||||||
|
cp "$tmp_dir/opt/pcatelegram_web/mieru_server_config.json" "$backup_dir/mieru_server_config.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Проверяем метаданные
|
# Проверяем метаданные
|
||||||
if [ -f "$backup_dir/metadata.json" ]; then
|
if [ -f "$backup_dir/metadata.json" ]; then
|
||||||
@@ -314,6 +326,16 @@ restore_backup() {
|
|||||||
cp "$backup_dir/warp.json" "$PCATELEGRAM_WEB_DIR/warp.json" 2>/dev/null
|
cp "$backup_dir/warp.json" "$PCATELEGRAM_WEB_DIR/warp.json" 2>/dev/null
|
||||||
chmod 600 "$PCATELEGRAM_WEB_DIR/warp.json" 2>/dev/null || true
|
chmod 600 "$PCATELEGRAM_WEB_DIR/warp.json" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$backup_dir/mieru.json" ]; then
|
||||||
|
mkdir -p "$PCATELEGRAM_WEB_DIR"
|
||||||
|
cp "$backup_dir/mieru.json" "$PCATELEGRAM_WEB_DIR/mieru.json" 2>/dev/null
|
||||||
|
chmod 600 "$PCATELEGRAM_WEB_DIR/mieru.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/mieru_server_config.json" ]; then
|
||||||
|
mkdir -p "$PCATELEGRAM_WEB_DIR"
|
||||||
|
cp "$backup_dir/mieru_server_config.json" "$PCATELEGRAM_WEB_DIR/mieru_server_config.json" 2>/dev/null
|
||||||
|
chmod 600 "$PCATELEGRAM_WEB_DIR/mieru_server_config.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Восстанавливаем language marker (i18n)
|
# Восстанавливаем language marker (i18n)
|
||||||
if [ -f "$backup_dir/.language" ]; then
|
if [ -f "$backup_dir/.language" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user